shiro是apache的一款开元安全框架,可以进行用户身份的认证,授权,session管理,加密等。相对于springsecurity来说比较简单,容易理解和使用。下来我们实现在springboot项目中使用shiro框架。
我们使用RBAC(基于角色的访问控制)来设计角色权限关系,有3个实体:用户,角色,权限,在RBAC中用户拥有某一种角色,角色拥有一个或者多个资源,所以表结构设计如下:
1 用户信息 2 DROP TABLE IF EXISTS `user_info`; 3 CREATE TABLE `user_info` ( 4 `id` int(11) NOT NULL AUTO_INCREMENT, 5 `username` varchar(50) DEFAULT NULL, 6 `password` varchar(32) DEFAULT NULL, 7 `email` varchar(50) DEFAULT NULL, 8 `usertype` char(1) DEFAULT NULL, 9 PRIMARY KEY (`id`) 10 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 11 12 角色信息 13 DROP TABLE IF EXISTS `role_info`; 14 CREATE TABLE `role_info` ( 15 `id` int(11) NOT NULL AUTO_INCREMENT, 16 `rolename` varchar(50) DEFAULT NULL, 17 `roledesc` varchar(100) DEFAULT NULL, 18 PRIMARY KEY (`id`) 19 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 20 21 用户角色关系 22 DROP TABLE IF EXISTS `user_role`; 23 CREATE TABLE `user_role` ( 24 `id` int(11) NOT NULL AUTO_INCREMENT, 25 `userid` int(11) NOT NULL, 26 `roleid` int(11) NOT NULL, 27 PRIMARY KEY (`id`) 28 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 29 30 资源(权限)信息 31 DROP TABLE IF EXISTS `resource_info`; 32 CREATE TABLE `resource_info` ( 33 `id` int(11) NOT NULL AUTO_INCREMENT, 34 `resourceName` varchar(50) NOT NULL, 35 `resourceUrl` varchar(50) NOT NULL, 36 `resourceType` char(1) NOT NULL COMMENT '1-菜单;2-按钮', 37 `resourcePid` int(11) DEFAULT NULL COMMENT '上级,按钮上级为菜单', 38 PRIMARY KEY (`id`) 39 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 40 41 角色资源关系 42 DROP TABLE IF EXISTS `role_resource`; 43 CREATE TABLE `role_resource` ( 44 `id` int(11) NOT NULL AUTO_INCREMENT, 45 `roleId` int(11) NOT NULL, 46 `resourceId` int(11) NOT NULL, 47 PRIMARY KEY (`id`) 48 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
下来实现springboot项目与shiro的整合,创建springboot项目并使用mybatis做数据库访问,不清楚springboot整合mybatis的朋友可以看我之前的笔记,我们假设已经有了项目,这里只说整合shiro。
1 在pom文件中添加对shiro的依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2 创建shiro配置信息类,完成配置
1 @Configuration 2 public class ShiroConfig { 3 4 /** 5 * shiro 过滤器 6 * @param securityManager 7 * @return 8 */ 9 @Bean 10 public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ 11 ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); 12 shiroFilter.setSecurityManager(securityManager); 13 shiroFilter.setLoginUrl("/login");//登录失败后默认跳转到登录地址去 14 shiroFilter.setSuccessUrl("/main");//登录成功后跳转地址 15 16 /** 17 * 过滤器链, 从前往后 顺序判断 18 */ 19 Map<String, String> filterChain = new LinkedHashMap<String, String>(); 20 //记住我或者认证登录通过可以访问 21 filterChain.put("/main", "user"); 22 23 filterChain.put("/js/**", "anon");//设置静态资源可以匿名访问 24 filterChain.put("/logout", "logout");//logout shiro已经做了实现 25 filterChain.put("/login", "anon");//可匿名访问 26 27 filterChain.put("/**", "authc");// 除了上边配置的路径及资源外,其他访问都需要认证,此项必须放在最后 28 shiroFilter.setFilterChainDefinitionMap(filterChain); 29 30 shiroFilter.setUnauthorizedUrl("/unAuthPage");//没有授权的页面 31 return shiroFilter; 32 } 33 34 /** 35 * 安全管理器 36 * @return 37 */ 38 @Bean 39 public DefaultWebSecurityManager securityManager(){ 40 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 41 securityManager.setRealm(getShiroRealm());//自定义realm 42 securityManager.setRememberMeManager(rememberMeManager());//记住我管理器 43 return securityManager; 44 } 45 46 /** 47 * 自定义的realm 48 * @return 49 */ 50 @Bean 51 public ShiroRealm getShiroRealm(){ 52 ShiroRealm shiroRealm = new ShiroRealm(); 53 shiroRealm.setCredentialsMatcher(credentialsMatcher());//凭证匹配器 54 return shiroRealm; 55 } 56 57 /** 58 * 凭证匹配器 (密码的校验交给了shiro的SimpleAuthenticationInfo处理) 59 * @return 60 */ 61 @Bean 62 public HashedCredentialsMatcher credentialsMatcher(){ 63 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); 64 credentialsMatcher.setHashAlgorithmName("MD5");//散列算法名称,如:md5 65 credentialsMatcher.setHashIterations(2);//散列算法的次数 66 return credentialsMatcher; 67 } 68 69 /** 70 * 记住我管理器 71 * @return 72 */ 73 @Bean 74 public CookieRememberMeManager rememberMeManager(){ 75 CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); 76 rememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag==")); 77 rememberMeManager.setCookie(cookie()); 78 return rememberMeManager; 79 } 80 81 /** 82 * 记住我 cookie 83 * @return 84 */ 85 @Bean 86 public SimpleCookie cookie(){ 87 SimpleCookie rememberMeCookie = new SimpleCookie("rememberMe"); 88 rememberMeCookie.setHttpOnly(true); 89 rememberMeCookie.setMaxAge(60);//记住我生效时间,单位 秒 90 return rememberMeCookie; 91 } 92 93 /** 94 * shiro生命周期 95 * @return 96 */ 97 @Bean 98 public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { 99 return new LifecycleBeanPostProcessor(); 100 } 101 102 }
2 自定义的realm,实现认证和授权
1 public class ShiroRealm extends AuthorizingRealm { 2 3 @Autowired 4 private UserInfoService userInfoService; 5 @Autowired 6 private UserRoleService userRoleService; 7 @Autowired 8 private ResourceInfoService resourceInfoService; 9 10 /** 11 * 授权 12 */ 13 @Override 14 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 15 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 16 Set<String> permissionsSet = new HashSet<String>();//用来存放用户所拥有的权限资源 17 //当前用户 18 UserInfoEntity userInfo = (UserInfoEntity) principals.getPrimaryPrincipal();//如果认证方法中保存的是用户账号,此处获取的是用户的账号,如果存的是用户对象,则此处获取的是用户对象 19 //当前用户角色 20 UserRole userRole = userRoleService.selectByUserId(userInfo.getId()); 21 if(userRole != null){ 22 List<ResourceInfo> resourceList = resourceInfoService.selectByRoleId(userRole.getRoleId()); 23 if(resourceList != null && resourceList.size()>0){ 24 for(int i=0; i<resourceList.size(); i++){ 25 ResourceInfo ri = resourceList.get(i); 26 permissionsSet.add(ri.getResourceUrl()); 27 } 28 } 29 } 30 info.setStringPermissions(permissionsSet); 31 return info; 32 } 33 34 /** 35 * 认证 36 */ 37 @Override 38 protected AuthenticationInfo doGetAuthenticationInfo( 39 AuthenticationToken authToken) throws AuthenticationException { 40 //将AuthenticationToken转换为UsernamePasswordToken 41 UsernamePasswordToken token = (UsernamePasswordToken) authToken; 42 //获取用户名 43 String username = token.getUsername(); 44 //判断当前用户名在数据库中是否存在 45 UserInfoEntity userInfo = userInfoService.selectByEmail(username); 46 if(userInfo == null){ 47 throw new UnknownAccountException("账户不存在"); 48 } 49 //从数据库对象获取的密码 50 String password = userInfo.getPassword(); 51 52 //认证的实体信息,可以是用户实体,也可以是用户账号 53 Object principal = userInfo; 54 //认证的密码 55 Object credentials = password; 56 //加密的颜 57 ByteSource credentialsSalt = ByteSource.Util.bytes(username);//使用登录的账号(唯一性)作为颜 58 59 //通过SimpleAuthenticationInfo 认证 60 SimpleAuthenticationInfo authInfo = null; 61 authInfo = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, this.realmName()); 62 return authInfo; 63 } 64 65 public String realmName(){ 66 return "shiroRealm"; 67 } 68 }
3 登录实现
1 @Controller 2 public class LoginController { 3 4 private static final Logger logger = LogManager.getLogger(LoginController.class); 5 6 @RequestMapping("/login") 7 public String login(){ 8 logger.info("跳转至登录页。。。"); 9 return "login"; 10 } 11 12 @RequestMapping(value="/login",method=RequestMethod.POST) 13 @ResponseBody 14 public String doLogin(@RequestParam("username") String username, @RequestParam("password") String password, 15 @RequestParam(value = "rememberMeParam", required = false) String rememberMeParam, Map<String, Object> map){ 16 UsernamePasswordToken token = new UsernamePasswordToken(username, password); 17 18 //记住我 19 if("Y".equals(rememberMeParam)){ 20 token.setRememberMe(true); 21 } 22 23 Subject subject = SecurityUtils.getSubject(); 24 try { 25 subject.login(token); 26 } catch (UnknownAccountException e) { 27 //e.printStackTrace(); 28 logger.info("账号不存在"); 29 } catch (IncorrectCredentialsException e) { 30 //e.printStackTrace(); 31 logger.info("密码不正确"); 32 } 33 34 if(subject.isAuthenticated()){ 35 map.put("result", "success"); 36 logger.info("登录成功。。。"); 37 }else{ 38 map.put("result", "faile"); 39 map.put("msg", "账号或密码错误"); 40 } 41 return JSON.toJSONString(map); 42 } 43 44 @RequestMapping("/main") 45 public String main(Model model){ 46 Subject subject = SecurityUtils.getSubject(); 47 UserInfoEntity userInfo = (UserInfoEntity) subject.getPrincipal(); 48 model.addAttribute("currentUser", userInfo); 49 return "main"; 50 } 51 52 @RequestMapping("/unAuthPage") 53 public String unAuthPage(){ 54 return "unAuth"; 55 } 56 57 @RequestMapping("/logout") 58 public String logout(){ 59 Subject subject = SecurityUtils.getSubject(); 60 subject.logout(); 61 return "redirect:/login"; 62 } 63 64 }
4 退出 退出功能shiro已经为我做了实现,我们只需要调用Subject的logout()方法即可实现用户退出。
5 测试
在没有登录的情况下,如果我们访问main方法,会被shiroFilter拦截,并将请求转向loginUrl方法引导用户登录;登录成功后会跳转至successUrl;
在页面上我们使用shiro的标签来做权限过滤,需要在页面引入shiro标签库:<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>,可以在菜单或者按钮上加上权限过滤标签如:
<shiro:hasPermission name="your perMission">
<a href=".....">用户管理</a>
</shiro:hasPermission>
在加载页面时,遇到shiro标签时会去后台查询用户权限信息,如果用户拥有当前标签中name属性指定的权限信息时会加载并显示当前shiro标签包含的页面元素信息,否则不会显示