SpringBoot中关于Shiro权限管理的整合使用
shiro是一个轻量级的安全框架,包含用户认证和用户授权
分析shiro的核心API:
Subject:用户主体(把操作交给SecurityManager)
SecurityManager:安全管理器(管理Reaml)
Reaml:shiro连接数据的桥梁
Shiro的配置类:
创建ShiroFilterFactoryBean;
创建DefaultWebSecurityManager;
创建Reaml(继承AuthorizingReaml);
Shiro内置过滤器,可以实现权限相关的拦截器:
常用的过滤器:
anno:无需认证(登陆)可以访问
authc:必须认证才能访问
user:如果使用rememberMe的功能可以直接访问
perms:该资源必须得到资源权限可以访问
role:该资源必须得到角色权限才能访问
在整合Shiro的时候,我们先要确定一下我们的步骤:
1.加入Shiro的依赖包,实现自己的Realm类(通过继承AuthorizingRealm类);
2.实现Shiro的配置类
3.实现前端的登录界面以及Controller类
第一步:
在pom.xml中加入依赖包
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
实现Realm类
package ariky.shiro.realm; import java.util.HashSet; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; 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.apache.shiro.web.subject.WebSubject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @ClassName: * @Description: Realm的配置 * @author fuweilian * @date 2018-5-12 上午11:36:41 */ public class MyShiroRealm extends AuthorizingRealm { //slf4j记录日志,可以不使用 private Logger logger = LoggerFactory.getLogger(MyShiroRealm.class); /** * 设置授权信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { logger.info("开始授权(doGetAuthorizationInfo)"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); HttpServletRequest request = (HttpServletRequest) ((WebSubject) SecurityUtils .getSubject()).getServletRequest();//这个可以用来获取在登录的时候提交的其他额外的参数信息 String username = (String) principals.getPrimaryPrincipal();//这里是写的demo,后面在实际项目中药通过这个登录的账号去获取用户的角色和权限,这里直接是写死的 //受理权限 //角色 Set<String> roles = new HashSet<String>(); roles.add("role1"); authorizationInfo.setRoles(roles); //权限 Set<String> permissions = new HashSet<String>(); permissions.add("user:list"); //permissions.add("user:add"); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } /** * 设置认证信息 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("开始认证(doGetAuthenticationInfo)"); //UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; HttpServletRequest request = (HttpServletRequest) ((WebSubject) SecurityUtils .getSubject()).getServletRequest(); UsernamePasswordToken token = new UsernamePasswordToken (request.getParameter("userName"),request.getParameter("password")); //获取用户输入的账号 String userName = (String)token.getPrincipal(); //通过userName去数据库中匹配用户信息,通过查询用户的情况做下面的处理 //这里暂时就直接写死,根据登录用户账号的情况做处理 logger.info("账号:"+userName); if("passwordError".equals(userName)){//密码错误 throw new IncorrectCredentialsException(); }else if("lockAccount".equals(userName)){// 用户锁定 throw new LockedAccountException(); }else{ SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userName, //用户名 "123456", //密码,写死 ByteSource.Util.bytes(userName+"salt"),//salt=username+salt getName() //realm name ); return authenticationInfo; } } }
第二步 实现Shiro的配置类:
package ariky.shiro.configuration; import java.util.LinkedHashMap; import java.util.Map; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import ariky.shiro.realm.MyShiroRealm; /** * @ClassName: ShiroConfiguration * @Description: shiro的配置类 * @author fuweilian * @date 2018-5-12 上午11:05:09 */ @Configuration public class ShiroConfiguration { private static Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class); @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ logger.info("进入shiroFilter......"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //设置不需要拦截的路径 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //按顺序依次判断 filterChainDefinitionMap.put("/static/**", "anon"); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> /************************************初始化所有的权限信息开始******************************************/ //这里,如果以后再项目中使用的话,直接从数据库中查询 filterChainDefinitionMap.put("/user/list", "authc,perms[user:list]"); //filterChainDefinitionMap.put("/user/add", "authc,perms[user:add]"); /***************************************初始化所有的权限信息开始结束*********************************************/ filterChainDefinitionMap.put("/**", "authc"); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面 shiroFilterFactoryBean.setUnauthorizedUrl("/error/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); //后面这里可以设置缓存的机制 return myShiroRealm; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
第三步:实现Controoler类,这里写俩个类,一个是登录信息的LoginController处理类,一个是测试权限用的UserController
1.LoginController.java
package ariky.controller; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; 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.RequestMethod; /** * @ClassName: LoginController * @Description: 登录控制的controller * @author fuweilian * @date 2018-5-12 下午01:15:46 */ @RequestMapping @Controller public class LoginController { private Logger logger = LoggerFactory.getLogger(LoginController.class); @RequestMapping(value="/login",method=RequestMethod.GET) public String getLogin(){ logger.info("进入login页面"); return "login"; } @RequestMapping(value="/login",method=RequestMethod.POST) public String doLogin(HttpServletRequest req,Map<String, Object> model){ logger.info("进入登录处理"); String exceptionClassName = (String) req.getAttribute("shiroLoginFailure"); logger.info("exceptionClassName:"+exceptionClassName); String error = null; if (UnknownAccountException.class.getName().equals(exceptionClassName)) { error = "用户名/密码错误"; } else if (IncorrectCredentialsException.class.getName().equals(exceptionClassName)) { error = "用户名/密码错误"; }else if(LockedAccountException.class.getName().equals(exceptionClassName)){ error = "用户已锁定或已删除"; }else if (exceptionClassName != null) { error = "其他错误:" + exceptionClassName; } if(SecurityUtils.getSubject().isAuthenticated()){//没有错误,但是已经登录了,就直接跳转到welcom页面 model.put("name", req.getParameter("userName")); return "index"; }else{//有错误的 model.put("error", error); return "login"; } } @RequestMapping("/index") public String index(){ return "index"; } }
2.UserController.java
package ariky.controller; import java.util.ArrayList; import java.util.List; import org.apache.shiro.authz.annotation.RequiresPermissions; 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.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * @ClassName: UserController * @Description: 用户处理Controller * @author fuweilian * @date 2018-5-12 下午03:11:06 */ @Controller @RequestMapping("/user") public class UserController { Logger logger = LoggerFactory.getLogger(UserController.class); @RequiresPermissions("user:list")//这个是配置是否有该权限的,如果是按上面的写法,这个是有权限的 @RequestMapping(value="/list",method=RequestMethod.GET) public String getList(){ logger.info("进入用户列表"); return "user/list"; } @RequiresPermissions(value={"user:add"})//这个是没有权限的 @RequestMapping(value="/add",method=RequestMethod.GET) public String getAdd(){ logger.info("进入新增用户界面"); return "user/add"; } }
前端界面:有5个界面 (login.jsp,index.jsp,list.jsp,add.jsp,403.jsp)
目录结构为:
1.login.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>Login</title> </head> <body> <h1>登录页面----${error}</h1> <form:form action="${pageContext.request.contextPath }/login" method="post"> 用户名:<input type="text" name="userName"> <br /> 密码:<input type="passwordParam" name="password"/> <input type="submit" value="提交"/> </form:form> </body> </html>
2.index.jsp
<%@ page language="java" pageEncoding="UTF-8"%> <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>第一个例子</title> <script src="${pageContext.request.contextPath }/webjars/jquery/2.1.4/jquery.js"></script> <script src="${pageContext.request.contextPath }/webjarslocator/jquery/jquery.js"></script> </head> <body> <h1>${name}:你好,欢迎访问该网页</h1> <shiro:hasPermission name="user:list"><!-- 这个a标签是可以看见的 --> <a href="${pageContext.request.contextPath }/user/list" target="_blank">跳转到用户列表(有权限)</a> </shiro:hasPermission> <br/> <shiro:hasPermission name="user:add"><!-- 这个a标签是看不见的 --> <a href="${pageContext.request.contextPath }/user/add" target="_blank">跳转到新增用户列表(无权限)</a> </shiro:hasPermission> </body> </html>
3.list.jsp和add.jsp以及403.jsp都差不多一样,这里就写一个,这里只是demo所用,在实际项目中,要以实际项目为准
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>userList</title>
</head>
<body>
<h1>用户列表信息</h1>
</body>
</html>
上面就是全部代码了,如果启动成功,进入login登录界面就可以测试一下shiro的权限认证了。上面的代码都是写死的,如果想要实现动态的权限管理和用户的权限管理的话,还要做一些其他处理,用户的动态权限这个只要在自己的ShiroRealm类里面授权的时候做一下查询数据库,动态的授权和角色就行。关于动态的权限管理的话,下面的方式可以实现,在修改完权限数据后,更新一下shiro里面的配置就行,具体看下面的代码,这里是demo,不是实际项目,在实际项目中最好不要把逻辑写在Controller里面
package ariky.shiro.controller; import java.util.LinkedHashMap; import java.util.Map; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager; import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; import org.apache.shiro.web.servlet.AbstractShiroFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * @ClassName: PermssionController * @Description: 权限操作的controller * @author fuweilian * @date 2018-5-12 下午04:59:15 */ @Controller @RequestMapping("permssion") public class PermssionController { @Autowired ShiroFilterFactoryBean shiroFilterFactoryBean; /** * @Title: updatePermssion * @author: fuweilian * @Description: 这里暂时直接写在controller里面,,不按规则写了,,到时候在项目中使用的时候,才写 * @return 参数说明 * @return Object 返回类型 * @throws */ @RequestMapping("/updatePermssion") @ResponseBody public Object updatePermssion(){ synchronized (shiroFilterFactoryBean){ AbstractShiroFilter shiroFilter = null; try { shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean .getObject(); PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter .getFilterChainResolver(); DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver .getFilterChainManager(); // 清空老的权限控制 manager.getFilterChains().clear(); shiroFilterFactoryBean.getFilterChainDefinitionMap().clear(); //后面这个可以直接从数据库里面获取 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //按顺序依次判断 filterChainDefinitionMap.put("/static/**", "anon"); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问--> /************************************初始化所有的权限信息开始******************************************/ //这里,如果以后再项目中使用的话,直接从数据库中查询 filterChainDefinitionMap.put("/user/list", "authc,perms[user:list]"); filterChainDefinitionMap.put("/user/add", "authc,perms[user:add]"); /***************************************初始化所有的权限信息开始结束*********************************************/ filterChainDefinitionMap.put("/**", "authc"); // shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未授权界面 shiroFilterFactoryBean.setUnauthorizedUrl("/error/403"); shiroFilterFactoryBean .setFilterChainDefinitionMap(filterChainDefinitionMap); // 重新构建生成 Map<String, String> chains = shiroFilterFactoryBean .getFilterChainDefinitionMap(); for (Map.Entry<String, String> entry : chains.entrySet()) { String url = entry.getKey(); String chainDefinition = entry.getValue().trim() .replace(" ", ""); manager.createChain(url, chainDefinition); } return "更新权限成功"; } catch (Exception e) { throw new RuntimeException( "更新shiro权限出现错误!"); } } } }
下面是mysql库的表结构
/* Navicat MySQL Data Transfer Source Server : arikyDB Source Server Version : 50721 Source Host : 47.106.95.168:3306 Source Database : ariky Target Server Type : MYSQL Target Server Version : 50721 File Encoding : 65001 Date: 2018-05-14 16:05:51 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for common_permssion -- ---------------------------- DROP TABLE IF EXISTS `common_permssion`; CREATE TABLE `common_permssion` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `NAME` varchar(255) DEFAULT NULL COMMENT '权限名称', `TYPE` varchar(255) DEFAULT NULL COMMENT '类型按钮(button)或者菜单(menu) ', `PARENT_ID` int(11) DEFAULT NULL COMMENT '上级ID', `PARENT_IDS` varchar(255) DEFAULT NULL COMMENT '上级PIDs', `URL` varchar(255) DEFAULT NULL COMMENT '访问路径', `ICONCLS` varchar(255) DEFAULT NULL COMMENT '图标(可以不要)', `PERMISSION` varchar(255) DEFAULT NULL COMMENT '权限(如user:list)', `ORDER_NUM` int(11) DEFAULT NULL COMMENT '排序', `REMARK` varchar(255) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`ID`) ) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8 COMMENT='该表用来存储资源权限信息'; -- ---------------------------- -- Table structure for common_role -- ---------------------------- DROP TABLE IF EXISTS `common_role`; CREATE TABLE `common_role` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `LABEL_ID` varchar(255) DEFAULT NULL COMMENT '标签Id', `NAME` varchar(255) DEFAULT NULL COMMENT '角色名称', `ROLE` varchar(255) DEFAULT NULL, `DESCRIPTION` varchar(255) DEFAULT NULL, `IS_SHOW` int(11) DEFAULT '1' COMMENT '判断该角色是否在使用(1:使用,2:禁用)', `IS_HANDLER` int(2) DEFAULT NULL COMMENT '判断是什么角色(1:后台角色,2:商家管理员角色,3:商家添加用户角色,4:游客角色)', PRIMARY KEY (`ID`) ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT='角色表'; -- ---------------------------- -- Table structure for common_role_permssion -- ---------------------------- DROP TABLE IF EXISTS `common_role_permssion`; CREATE TABLE `common_role_permssion` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id', `ROLE_ID` int(11) DEFAULT NULL COMMENT '角色Id', `RESOURCE_ID` int(11) DEFAULT NULL COMMENT '资源(权限)Id', PRIMARY KEY (`ID`) ) ENGINE=InnoDB AUTO_INCREMENT=493 DEFAULT CHARSET=utf8 COMMENT='角色资源权限表中间表';