最近公司一个老的后台管理系统需要加权限验证(前端使用easyUI),经过各种踩坑,终于是完成了,话不多说,直接进入主题
数据库结构
用户表(使用的项目本身已经在用的表)
CREATE TABLE `pay_user` (
`ID` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'ID主键',
`USER_NAME` VARCHAR(45) NOT NULL COMMENT '登录名称',
`PASSWORD` VARCHAR(50) NOT NULL COMMENT '密码',
`STATUS` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '状态:0:正常,1:禁用;default:0',
`CREATE_TIME` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UPDATE_TIME` DATETIME NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`ID`),
UNIQUE INDEX `ID_UNIQUE` (`ID`) USING BTREE,
UNIQUE INDEX `USER_NAME_UK` (`USER_NAME`) USING BTREE
)
COMMENT='运营管理用户表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=48
;
菜单表
CREATE TABLE `sys_menu` (
`id` BIGINT(8) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(64) NOT NULL COMMENT '菜单名称',
`icon` VARCHAR(64) NOT NULL DEFAULT 'icon-nav' COMMENT '图标',
`url` VARCHAR(255) NULL DEFAULT NULL COMMENT '菜单路径',
`pid` BIGINT(8) UNSIGNED NOT NULL DEFAULT '0' COMMENT '父级资源id',
`sort` TINYINT(2) NOT NULL DEFAULT '0' COMMENT '排序',
`permission_code` VARCHAR(50) NULL DEFAULT NULL COMMENT '权限层级',
`status` TINYINT(2) NOT NULL DEFAULT '0' COMMENT '状态',
`menu_flag` TINYINT(1) NULL DEFAULT '0' COMMENT '是否是菜单 0主菜单 1页面 2按钮',
`description` VARCHAR(255) NULL DEFAULT NULL COMMENT '备注',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
)
COMMENT='菜单'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=297
;
角色表(这里当时有个设计缺陷就是应该多加一个code字段)
CREATE TABLE `sys_role` (
`id` BIGINT(19) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`name` VARCHAR(64) NOT NULL COMMENT '角色名',
`sort` TINYINT(2) NOT NULL DEFAULT '0' COMMENT '排序号',
`description` VARCHAR(255) NULL DEFAULT NULL COMMENT '描述',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
)
COMMENT='角色'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=7
;
角色-菜单关系表
CREATE TABLE `sys_role_menu` (
`id` BIGINT(8) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`role_id` BIGINT(8) NOT NULL COMMENT '角色id',
`menu_id` BIGINT(8) NOT NULL COMMENT '菜单id',
PRIMARY KEY (`id`)
)
COMMENT='角色-菜单关系表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=2763
;
用户-角色关系表
CREATE TABLE `sys_user_role` (
`id` BIGINT(8) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` BIGINT(8) NOT NULL COMMENT '用户id',
`role_id` BIGINT(8) NOT NULL COMMENT '角色id',
PRIMARY KEY (`id`)
)
COMMENT='用户-角色关系表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=28
;
1.首先引入依赖
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
当然像ehcache如果用不到可以不添加
2.web.xml配置
添加配置
<!--<!–Shiro配置 DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来–>-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置true由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3.spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- shiro -->
<!-- 1. 将Shiro的生命周期加入Spring
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
-->
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1"/>
</bean>
<!-- 2. 自定义Realm来管理权限 -->
<bean id="shiroDbRealm" class="com.joiest.jpf.manage.web.shiro.ShiroDbRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!--<!– 缓存管理器 –>-->
<!--<bean id="cacheMnager" class="org.apache.shiro.cache.ehcache.EhCacheManager">-->
<!--<property name="cacheManagerConfigFile" value="classpath:spring/shiro-ehcache.xml"/>-->
<!--</bean>-->
<!--<!– 会话管理器 –>-->
<!--<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">-->
<!--<!– session的失效时长,单位毫秒 –>-->
<!--<property name="globalSessionTimeout" value="1800000"/>-->
<!--<!– 删除失效的session –>-->
<!--<property name="deleteInvalidSessions" value="true"/>-->
<!--</bean>-->
<!-- 3. 将自定义Realm赋值给securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroDbRealm"/>
<!--<property name="sessionManager" ref="sessionManager" />-->
<!--<property name="cacheManager" ref="cacheMnager" />-->
</bean>
<!-- 4. 配置SecurityUtils将securityManager-->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>
<!-- 5. 配置shiroFilter-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 要求登录时的链接(登录页面地址),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/"/>
<!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在页面处理的) -->
<property name="successUrl" value="/backIndex.jsp" ></property>
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="/refuse.jsp"></property>
<!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap">
</property>
<property name="filterChainDefinitions">
<value>
<!-- anon不需要认证-->
<!--/login = anon-->
/resources/** = anon
/user/login = anon
/user/logout = anon
<!--这里可以控制地址栏输入url也进行拦截,但是这样配置太繁琐,下篇文章会扩展-->
/user/index = perms["userManage:all"]
<!--/user/index = anon-->
<!--所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过登录验证,如果未登录则跳到/login -->
/** = authc
</value>
</property>
</bean>
</beans>
4.创建自定义Realm
package com.joiest.jpf.manage.web.shiro;
import com.joiest.jpf.common.po.SysMenu;
import com.joiest.jpf.common.po.SysRoleMenu;
import com.joiest.jpf.common.po.SysUserRole;
import com.joiest.jpf.common.util.Md5Encrypt;
import com.joiest.jpf.common.util.SHA1;
import com.joiest.jpf.entity.UserInfo;
import com.joiest.jpf.facade.SysMenuFacade;
import com.joiest.jpf.facade.SysRoleMenuFacade;
import com.joiest.jpf.facade.SysUserRoleFacade;
import com.joiest.jpf.facade.UserServiceFacade;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class ShiroDbRealm extends AuthorizingRealm {
@Autowired
private UserServiceFacade userServiceFacade;
@Autowired
private SysUserRoleFacade sysUserRoleFacade;
@Autowired
private SysRoleMenuFacade sysRoleMenuFacade;
@Autowired
private SysMenuFacade sysMenuFacade;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=================doGetAuthenticationInfo[登录认证]=====================");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
System.out.println("当前认证用户:" + token.getUsername());
UserInfo user = userServiceFacade.getByLoginName(token.getUsername());
if (user != null) {
ShiroUser shiroUser = new ShiroUser(user.getUserName());
shiroUser.setId(user.getId()+"");
shiroUser.setName(user.getUserName());
//以创建时间作为盐(加密用的)
// String salt = String.valueOf(user.getCreateTime().getTime());
return new SimpleAuthenticationInfo(shiroUser, Md5Encrypt.md5(user.getPassword()), getName());
}
return null;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("=================doGetAuthorizationInfo[授权认证]=====================");
ShiroUser shiroUser = (ShiroUser) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<String> permissions = new ArrayList<>();
//由于炒鸡管理员权限太多,加载每次点击菜单加载数据很慢,所以这里用了缓存,只第一次加载
if (shiroUser.getPermissions()==null || shiroUser.getPermissions().size() == 0){
//获取所有权限
List<SysUserRole> sysUserRoles =sysUserRoleFacade.selectRolesByUserId(shiroUser.getId());
for (SysUserRole sysUserRole : sysUserRoles) {
List<SysRoleMenu> sysRoleMenus = sysRoleMenuFacade.getRoleMenuByRoleId(sysUserRole.getRoleId());
if (sysRoleMenus.size()>0){
for (SysRoleMenu sysRoleMenu : sysRoleMenus) {
SysMenu menu = sysMenuFacade.getMenuById(sysRoleMenu.getMenuId());
if(menu!=null&& StringUtils.isNotBlank(menu.getPermissionCode())){
permissions.add(menu.getPermissionCode());
}
}
}
}
shiroUser.setPermissions(new HashSet<>(permissions));
info.addStringPermissions(permissions);
}else{
//info里存的就是所有的权限
info.addStringPermissions(shiroUser.getPermissions());
}
return info;
}
}
5.User类 shiroUser
package com.joiest.jpf.manage.web.shiro;
import java.io.Serializable;
import java.util.Set;
public class ShiroUser implements Serializable {
private String id;
private final String loginName;
private String name;
private Set<String> permissions;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public ShiroUser(String loginName) {
this.loginName = loginName;
}
public String getLoginName() {
return loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
}
6.Tree
package com.joiest.jpf.entity;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
public class Tree implements Serializable {
private Integer id;
private String text;
private int seq;
private boolean checked = false;// true,false
private boolean onlyLeafCheck = false;
private String state = "open";
private String code;
private List<Tree> children;
private String iconCls;
private Integer pid;
private Integer isLeaf;
private String url;
private String createTime;
private String updateTime;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public boolean getOnlyLeafCheck() {
return onlyLeafCheck;
}
public void setOnlyLeafCheck(boolean onlyLeafCheck) {
this.onlyLeafCheck = onlyLeafCheck;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public boolean getChecked() {
return checked;
}
public void setChecked(boolean checked) {
this.checked = checked;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public List<Tree> getChildren() {
return children;
}
public void setChildren(List<Tree> children) {
this.children = children;
}
public String getIconCls() {
return iconCls;
}
public void setIconCls(String iconCls) {
this.iconCls = iconCls;
}
public Integer getPid() {
return pid;
}
public void setPid(Integer pid) {
this.pid = pid;
}
public Integer getIsLeaf() {
return isLeaf;
}
public void setIsLeaf(Integer isLeaf) {
this.isLeaf = isLeaf;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getSeq() {
return seq;
}
public void setSeq(int seq) {
this.seq = seq;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public String getUpdateTime() {
return updateTime;
}
public void setUpdateTime(String updateTime) {
this.updateTime = updateTime;
}
}
7.登录 调用shiro的login()方法
@RequestMapping(value = "/user/login", method = RequestMethod.POST)
public ModelAndView login(HttpServletRequest request,HttpSession httpSession, Model model) {
String loginName = request.getParameter("user");
String password = request.getParameter("pwd");
Subject user = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginName, SHA1.getInstance().getMySHA1Code(password));
if (StringUtils.isBlank(loginName)||StringUtils.isBlank(password)) {
model.addAttribute("error", "用户名或密码错误");
return new ModelAndView("login");
}
UserInfo info = userServiceFacade.getByLoginName(loginName);
if (info == null){
model.addAttribute("error", "用户不存在或者密码错误!");
return new ModelAndView("login");
}
if (!EnumConstants.UserStatus.normal.value().equals(info.getStatus())){
model.addAttribute("error", "用户已禁用!");
return new ModelAndView("login");
}
List<SysUserRole> userRoles = sysUserRoleFacade.selectRolesByUserId(info.getId() + "");
if (userRoles.size() == 0){
model.addAttribute("error", "用户未分配角色,请联系管理员!");
return new ModelAndView("login");
}
try {
//shiro的login()方法
user.login(token);
} catch (UnknownAccountException e) {
model.addAttribute("error", "用户不存在或者密码错误!");
return new ModelAndView("login");
} catch (IncorrectCredentialsException ex) {
model.addAttribute("error","用户不存在或者密码错误!") ;
return new ModelAndView("login");
} catch (AuthenticationException ex) {
model.addAttribute("error","用户名或密码错误");
return new ModelAndView("login");
} catch (Exception ex) {
model.addAttribute("error","内部错误,请重试!");
return new ModelAndView("login");
}
PayUser userInfo = userServiceFacade.loginVerify(loginName, password);
//登录信息存进session
httpSession.setAttribute(ManageConstants.USERINFO_SESSION,userInfo);
httpSession.setMaxInactiveInterval(30*60);//以秒为单位,即在没有活动30分钟后,session将失效
return new ModelAndView("/backIndex");
}
8.跳转到首页后发送ajax请求(/menu)查询当前用户下的所有菜单
controller
@RequestMapping("/menu")
@ResponseBody
public Object getMenu(){
UserInfo sessionUser = SessionUtil.getSessionUser();
Integer id = sessionUser.getId();
List<Tree> treeList = sysMenuFacade.treeList(id);
JSONArray jsonArray = JSONArray.parseArray(JSON.toJSONString(treeList));
return jsonArray;
}
serviceImpl
@Override
public List<Tree> treeList(Integer id) {
List<SysMenu> menuList = sysMenuMapper.selectMenuListByUserId(id);
List<Tree> treeList = prepareTree(menuList);
//排序调整菜单顺序
Collections.sort(treeList, new Comparator<Tree>() {
@Override
public int compare(Tree t1, Tree t2) {
return t1.getSeq() - t2.getSeq();
}
});
return treeList;
}
private List<Tree> prepareTree(List<SysMenu> menuList) {
List<Tree> allTreeList = menuListToTreeList(menuList);
List<Tree> topList = new ArrayList<>();
for (Tree tree : allTreeList) {
// 遍历所有节点,将父菜单id与传过来的id比较
if (tree.getPid() == 0) {
tree.setChildren(prepareTreeChiled(tree.getId(), allTreeList));
topList.add(tree);
Collections.sort(topList, new Comparator<Tree>() {
@Override
public int compare(Tree o1, Tree o2) {
return o1.getSeq() - o2.getSeq();
}
});
}
}
return topList;
}
private List<Tree> prepareTreeChiled(Integer id, List<Tree> allTreeList) {
// 子菜单
List<Tree> childList = new ArrayList<>();
for (Tree tree : allTreeList) {
// 遍历所有节点,将父菜单id与传过来的id比较
//这里有个坑 因为常量池里127 == 127是成立的 129 == 129不成立 所以比较用equals
if (!"0".equals(tree.getPid()+"") && (id+"").equals(tree.getPid()+"")) {
childList.add(tree);
Collections.sort(childList, new Comparator<Tree>() {
@Override
public int compare(Tree o1, Tree o2) {
return o1.getSeq() - o2.getSeq();
}
});
}
}
// 把子菜单的子菜单再循环一遍
for (Tree tree : childList) {
tree.setChildren(prepareTreeChiled(tree.getId(), allTreeList));
}
// 递归退出条件
if (childList.size() == 0) {
return null;
}
return childList;
}
private List<Tree> menuListToTreeList(List<SysMenu> menuList) {
List<Tree> treeList = new ArrayList<>();
if (menuList != null && menuList.size() > 0) {
for (SysMenu menu : menuList) {
treeList.add(menuToTree(menu));
}
}
return treeList;
}
private Tree menuToTree(SysMenu menu) {
Tree tree = new Tree();
tree.setId(Integer.valueOf(menu.getId()));
tree.setText(menu.getName());
tree.setIconCls(menu.getIcon());
tree.setIsLeaf(Integer.valueOf(menu.getMenuFlag()));
tree.setPid(Integer.valueOf(menu.getPid()));
tree.setSeq(menu.getSort());
tree.setUrl(menu.getUrl());
tree.setCode(menu.getPermissionCode());
tree.setChecked(menu.getChecked());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String createTime = formatter.format(menu.getCreateTime());
if (menu.getUpdateTime() != null){
String updateTime = formatter.format(menu.getUpdateTime());
tree.setUpdateTime(updateTime);
}
tree.setCreateTime(createTime);
return tree;
}
selectMenuListByUserId sql
<select id="selectMenuListByUserId" resultMap="BaseResultMap">
select distinct m.id,m.name,m.icon,m.url,m.pid,m.sort,m.permission_code,m.`status`,m.menu_flag,
m.description,m.create_time,m.update_time
from sys_menu m
left join sys_role_menu rm
on m.id = rm.menu_id
left join sys_role r
on r.id = rm.role_id
left join sys_user_role ur
on ur.role_id = r.id
left join pay_user u
on u.id = ur.user_id
where u.id = #{id}
and m.menu_flag != 2
</select>
8.页面配置
//menu:add 是你表中自定义的权限code
<shiro:hasPermission name=“menu:add”>
//这里是你要控制的按钮代码
</shiro:hasPermission>
这样只要你的用户下有menu:add这个权限,就可以看到被shiro标签包含的按钮,反之则看不到
9.至于上面配置文件中提到的:
/user/index = perms[“userManage:all”]
这个是用来防止有人直接在地址栏输入地址的,你可以尝试一下,一个用户没有某个页面的权限,但是直接地址栏输入地址还是可以访问的,只不过没有权限的按钮不会显示出来,但这样明显是不可行的,所以每多一个页面都需要在配置文件配置这个参数,这样很繁琐,下一篇文章会介绍一种自定义filterChainDefinitionMap的方式