权限认证
实例工厂FilterChainDefinitionMapFactory类中,将写死的需要授权才能访问的资源的代码修改查询数据库获取到哪些资源需要授权才能访问
//需要授权才能访问的资源
//查询所有权限
List<Permission> list = permissionService.findAll(new PermissionQuery());
list.forEach(permission -> {
//将查询到的资源放入map中
map.put(permission.getUrl(),"myperms["+permission.getSn()+"]");
});
获取当前登陆用户拥有的权限
首先在员工实力类中添加与角色的多对对关联关系
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "employee_role",
joinColumns = {@JoinColumn(name = "employee_id")},
inverseJoinColumns = {@JoinColumn(name = "role_id")})
private List<Role> roles = new ArrayList<>();
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
角色表已经与权限表进行了关联,这样员工、角色、权限三张表通过Jpa配置的关系就关联起来了
Service中添加通过员工Id查询权限的方法
Service
Set<String> findPermissionByEmployeeId(Long id);
ServiceImpl
@Override
//查询当前登录的用户拥有的权限
public Set<String> findPermissionByEmployeeId(Long id) {
String Jpql = "select p from Employee e join e.roles r join r.permissions p where e.id=?1";
List<Permission> list = permissionRepository.findByJpql(Jpql, id);
Set<String> set = new HashSet<>();
list.forEach(permission -> {
set.add(permission.getSn());
});
return set;
}
这样就完成了哪些资源需要授权及当前登录用户拥有的权限的查询
自定义扩展过滤器
需要扩展的原因:
当admin1没有删除权限,却进行删除员工操作时,既删不掉员工数据,也没有任何提示消息,因为前端提交的删除请求是采用的异步请求,并且删除操作是必须授权按此能进行访问,所以admin1进行删除操作时,就会被Shiro拦截,重定向到了unauthorizedUrl中,但此操作是异步请求,不会跳转页面,也没有提示消息,Shiro内部权限认证过滤器只能处理同步请求的无权限情况,不能处理异步请求的无权限的情况,
所以需要自定义扩展过滤器
Shiro内部的权限认证过滤器代码:
//access 访问 Denied 拒绝 限制
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
//获取当前用户(不论是否已经登录)
Subject subject = this.getSubject(request, response);
//判断当前主体对象是否为null,为null就表示未登录,也就是判断是否已经登录
if (subject.getPrincipal() == null) {
//如果未登录,就重定向到loginUrl(是在shiro的配置文件中配置的)
this.saveRequestAndRedirectToLogin(request, response);
} else {
//如果是已登录,就获取到shiro的配置文件中的unauthorizedUrl
String unauthorizedUrl = this.getUnauthorizedUrl();
//判断unauthorizedUrl字符串不为null,也不为""
if (StringUtils.hasText(unauthorizedUrl)) {
//如果unauthorizedUrl字符串不为null,就重定向到unauthorizedUrl
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
//如果unauthorizedUrl字符串为null,或者"",就报401错误
WebUtils.toHttp(response).sendError(401);
}
}
return false;
}
Shiro过滤器中的权限认证代码是perms,perms就是Shiro内部map集合中的键。
Perms对应的过滤器就是PermissionsAuthorizationFilter,Shiro内部默认使用此类的父类AuthorizationFIlter中的onAccessDenied方法来处理资源无权限的情况
OnAccessDenied:当访问被限制、拒绝时
根据Shrio内部的权限认证过滤器发现它的处理方式就是重定向,而重定向和转发都是 同步跳转页面,没有处理异步请求的代码,所以在admin1执行删除操作时,不会跳转页面也没有任何的提示信息
同步请求与异步请求的区别
如果是异步请求,请求头中会有X-Requested-With,而且值一定是XMLHttpRequest,并且 XMLHttpRequest就是AJAX的核心对象。
所以只需要判断请求头信息中是否包含X-Requested-With,如果包含,就一定是异步请求
扩展实现
1、自定义一个MyPermissionsAuthorizationFilter类,继承PermissionsAuthorizationFilter类,然后重写 onAccessDenied方法
2、首先将ServletRequest、ServletResponse强转为HttpServletRequest、HttpServletResponse获取请求头消息
3、进行判断,判断请求头中是否包含XMLHttpRequest,如果存在就说明是异步请求,如果不存在就说明是同步请求,只需要调用父类的方法进行处理即可
public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//获取请求头消息
String xRequestedWith = req.getHeader("X-Requested-With");
//判断请求头中是否存在XMLHttpRequest
if ("XMLHttpRequest".equals(xRequestedWith)){
//如果存在就说明是异步请求
Result result = new Result(500,"对不起,您没有操作此功能权限");
//然后将result消息转化为JSON字符串
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
//使用响应对象的输出流来将此JSOn对象输出
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(json);
resp.getWriter().flush();
resp.getWriter().close();
}else {
//如果是同步请求就使用父类处理的方式:重定向
return super.onAccessDenied(request,response);
}
return false;
}
}
然后在自定义过滤器后需要通过配置告诉Shiro
public class FiltersMapFactory {
//自定义过滤器指定键名称 拿来和Shiro自带的过滤器存储的map集合合并
public Map<String, Filter> getFiltersMap() {
Map<String, Filter> map = new HashMap<String, Filter>();
//把自定义的过滤器 作为值放入map集合中
//键不能和anon,authc,perms等Shiro自带的过滤器名称相同,重复了会覆盖之前的过滤器
map.put("myperms", new MyPermissionsAuthorizationFilter());
return map;
}
}
Shiro.xml配置bean
<!--自定义的过滤器bean标签 拓展Shiro过滤器-->
<bean id="filtersMapFactory" class="cn.yinsh.ibs.shiro.FiltersMapFactory"/>
<bean id="filtersMap" factory-bean="filtersMapFactory" factory-method="getFiltersMap"/>
引用
<property name="filters" ref="filtersMap"/>
Shiro:hasPermission标签
hasPermission :判断当前登录用户是否拥有employee:delete权限,如果有就显示标签内部的内容
<shiro:hasPermission name="employee:delete"></shiro:hasPermission>
菜单功能
数据具有层级关系,比如有省份城市区县、菜单(一级、二级、三级…)、商品类别(一级、二级、 三级…)就需要使用自连接
自连接设计的数据库表结构:表中一定会有一个外键列,关联本表的主键,也就是说当前表的数据与当前表的数据构成一对多关 系,通常情况下这个外键列名称是parent_id或者father_id
自连接的domain配置:
menu:
//如果当前对象当作子菜单,那么它的父级菜单就不为null,如果父级菜单为null,那么此对象就是父级菜单
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
//@JsonIgnore
private Menu parent;
@Transient//表示是临时字段,查询数据后不会自动查询它的子菜单
private List<Menu> children = new ArrayList<>();
permission
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_id")
@JsonIgnoreProperties({"hibernateLazyInitializer","handler","fieldHandler"})
private Menu menu;
查询菜单:
在service层通过员工Id查询当前登录用户拥有的权限的菜单:手
Service:
List<Menu> findByEmployeeId(Long id);
ServiceImpl:
@Override
public List<Menu> findByEmployeeId(Long id) {
String jpql = "select m from Employee e join e.roles r join r.permissions p join p.menu m where e.id = ?1";
List<Menu> list = menuRepository.findByJpql(jpql, id);
//创建用于存放一级菜单的list集合
List<Menu> firstLevelMenus = new ArrayList<>();
//遍历查询出来的所有二级菜单数据
for (Menu secondLevelMenu: list) {
//获取到二级菜单的父级菜单
Menu parent = secondLevelMenu.getParent();
//去重:判断集合中如果没有当前一级菜单才进行添加
if (!firstLevelMenus.contains(parent)){
//将父级菜单添加到firstLecelMenus中
firstLevelMenus.add(parent);
}
//最后将二级菜单设置为当前父级菜单的子菜单
parent.getChildren().add(secondLevelMenu);
}
return firstLevelMenus;
}
Controller
@ResponseBody
@RequestMapping("findByEmployeeId")
//通过当前登录的用户来查询拥有的哪些菜单
public List<Menu> findBypEmployeeId(){
//获取当前登陆用户
Employee employee = (Employee) SecurityUtils.getSubject().getPrincipal();
return menuService.findByEmployeeId(employee.getId());
}
在欢迎界面的树形菜单中的url里面填写地址来访问这个controller方法
url:'/menu/findByEmployeeId'
问题:
1、查询菜单后发现展示的菜单数据全是undefind
原因:
easyui的tree组件中的每一个节点对象有以下属性:
id:节点ID,对加载远程数据很重要。
text:显示节点文本
state:节点状态,
open’ 或 ‘closed’,默认:‘open’。如果为’closed’的时候,将不自动展开该节 点 checked:表示该节点是否被选中。
attributes: 被添加到节点的自定义属性。
children: 一个节点数组声明了若干节点。
iconCls:菜单的图标样式
经过观察发现tree的节点对象的属性是text,而我们查询之后从后端返回的json数据中只有name表示名称
解决方法:
将getName方法复制一份,改名为getText
public String getText() {
return name;
}
这个做法是运用JavaBean的特性,是通过setget方法名称来确定属性名称的。JavaBean对象的属性名 称只与getset方法名称有关,与字段名称无关
2、显示出来的数据都是二级菜单,而且是当前登录用户拥有的二级菜单,因为权限表中只关 联了二级菜单的id
为了在EasyUI的tree组件能显示出父级关系的菜单,那么就需要返回json格式的数据,也就是,应该返回的是一级菜单,而不是二级菜单,也就是MenuController类中方法返回的List
//创建用于存放一级菜单的list集合
List<Menu> firstLevelMenus = new ArrayList<>();
//遍历查询出来的所有二级菜单数据
for (Menu secondLevelMenu: list) {
//获取到二级菜单的父级菜单
Menu parent = secondLevelMenu.getParent();
//去重:判断集合中如果没有当前一级菜单才进行添加
if (!firstLevelMenus.contains(parent)){
//将父级菜单添加到firstLecelMenus中
firstLevelMenus.add(parent);
}
//最后将二级菜单设置为当前父级菜单的子菜单
parent.getChildren().add(secondLevelMenu);
}
return firstLevelMenus;
3、JSON死循环(双向关系造成的)
在Menu实体类上使用
将字段patent添加上去
@Entity
**@JsonIgnoreProperties({"hibernateLazyInitializer","handler","fieldHandler","parent"})**
public class Menu extends BaseDomain {
}
4、菜单展示出来后发现有很多重复的菜单,需要去重
//去重:判断集合中如果没有当前一级菜单才进行添加
if (!firstLevelMenus.contains(parent)){
//将父级菜单添加到firstLecelMenus中
firstLevelMenus.add(parent);
}
修改菜单的图标
1、首先将图片添加到easyui/themes/icons中
2、修改icon.css,添加以下代码
.icon-lala{
background:url('icons/lala.png') no-repeat center center;
}
.icon-shezhi{
background:url('icons/shezhi.png') no-repeat center center;
}
.icon-shujuzidian{
background:url('icons/shujuzidian.png') no-repeat center center;
}
menu实体类get方法修改:
public String getIconCls() {
return icon;
}
数据库Icon字段