spring-boot+mybatisPlus+shiro的集成demo 我用了5天
spring-boot + mybatis-plus + shiro 的集成demo我用了五天
关于shiro框架,我还是从飞机哪里听来的,就连小贱都知道,可我母鸡啊。简单百度了下,结论很好上手,比spring的security要简单许多...于是我就是开始了我的shiro学习之路 。正巧这几天在研究spring-boot集成mybatis-plus 于是乎我就把shiro也揉了进去,但是效果并不像我预期想象的那样。
以下是我这几天血泪换来的成果-->
基本概念
shiro :隶属Apache 简单易用的java安全框架 三大件 :Subject, SecurityManager 和 Realm
Subject:即当前操作用户
SecurityManager:安全管理器
Realm:用户数据的概念,域
过程:
1)建表 数据库一般至少有五张表
1-user:用户账号密码
2-role:角色ID,一个账户可以有很多角色
3-permission权限ID,一个角色可以有很多权限
4-user_role关系对照表:记录每个userID有的角色
5-role_permission关系对照表,记录每个role有的permission
-- 用户表 DROP TABLE IF EXISTS `user_info`; CREATE TABLE `user_info` ( `uid` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, `pass_word` varchar(255) DEFAULT NULL COMMENT '密码', `salt` varchar(255) DEFAULT NULL COMMENT '加密', `state` tinyint(4) NOT NULL COMMENT '状态', `username` varchar(255) DEFAULT NULL COMMENT '用户名', `email` varchar(64) DEFAULT NULL COMMENT '邮箱', `crtime` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`uid`), UNIQUE KEY(`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 角色表 DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `available` bit(1) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, `role` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- 用户角色表 DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `uid` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY(`uid`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 权限表 DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `id` int(11) NOT NULL COMMENT 'PMK', `available` bit(1) DEFAULT NULL COMMENT '是否激活', `name` varchar(255) DEFAULT NULL, `parent_id` bigint(20) DEFAULT NULL, `parent_ids` varchar(255) DEFAULT NULL, `permission` varchar(255) DEFAULT NULL COMMENT '权限', `resource_type` enum('menu','button') DEFAULT NULL, `url` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 角色权限表 DROP TABLE IF EXISTS `sys_role_permission`; CREATE TABLE `sys_role_permission` ( `permission_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY(`role_id`,`permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2)项目搭建
spring-boot mybatis-plus shiro
1.pom.xml 贴出部分
<!-- mybits-plus -starter--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatisplus-spring-boot-starter</artifactId> <version>1.0.5</version> </dependency> <!-- MP 核心库 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>2.1.8</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- 模板引擎 代码生成 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity</artifactId> <version>1.7</version> </dependency> <!-- mybits-plus -end--> <!--shiro 登录认证--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>5.1.35</scope> </dependency> <!--druid 数据库连接池监控--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.0</version> </dependency> <!--jasypt 数据库加解密--> <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>1.8</version> </dependency>
2.先集成mybatis-plus 利用自动生成方法生成 新建5张表的POJO 和 Mapper Service文件
3.编写ShiroConfig配置类
package com.zxt.ms.configs; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; /** * @ClassName ShiroConfig * @Description ms 梦想家 * @Author Zhai XiaoTao https://www.cnblogs.com/zhaiyt * @Date 2019/1/26 17:27 * @Version 1.0 */ @Slf4j @Configuration public class ShiroConfig { @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * shiro自带过滤器,无需再另外设置filter * * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { log.info("ShiroConfig.shirFilter() start ..."); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); //配置过滤器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //没登录的页面 shiroFilterFactoryBean.setLoginUrl("/notLogin"); // 设置无权限时跳转的 url; shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); /* * shiro 内置枚举 * anon 表示可以匿名使用 * authc 表示需要认证(登录)才能使用,没有参数 */ //静态资源允许访问 filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/images/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/layer/**", "anon"); //游客,开发权限 filterChainDefinitionMap.put("/guest/**", "anon"); //用户,需要角色权限 “user” filterChainDefinitionMap.put("/user/**", "roles[user]"); //管理员,需要角色权限 “admin” filterChainDefinitionMap.put("/admin/**", "roles[admin]"); //开放登陆接口 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/loginUser", "anon"); //其余接口一律拦截 //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 filterChainDefinitionMap.put("/**", "authc"); //过滤器注入工厂类 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); log.info("ShiroConfig.shirFilter() end ..."); return shiroFilterFactoryBean; } /** * @return org.apache.shiro.mgt.SecurityManager * @Description <安全管理器Bean> * @Author Zhaiyt * @Date 14:35 2019/1/28 * @Param **/ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //注入realm securityManager.setRealm(myShiroRealm()); return securityManager; } /** * @return com.zxt.ms.configs.ShiroRealm * @Description <域 的概念 Shiro 从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份, * 那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作; * 可以把Realm看成DataSource , 即安全数据源> * @Author Zhaiyt * @Date 14:38 2019/1/28 * @Param **/ @Bean public ShiroRealm myShiroRealm() { ShiroRealm myShiroRealm = new ShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } /** * @return org.springframework.web.servlet.handler.SimpleMappingExceptionResolver * @Description <异常处理> * @Author Zhaiyt * @Date 15:20 2019/1/28 * @Param **/ @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理 mappings.setProperty("UnauthorizedException", "403"); exceptionResolver.setExceptionMappings(mappings); // None by default exceptionResolver.setDefaultErrorView("error"); // No default exceptionResolver.setExceptionAttribute("ex"); // Default is "exception" return exceptionResolver; } /** * 因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。 * @Description <加密> * @Author Zhaiyt * @Date 16:18 2019/1/29 * @Param * @return org.apache.shiro.authc.credential.HashedCredentialsMatcher **/ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; } /** * @Description <因为只有开启了AOP才执行doGetAuthorizationInfo(),也就权限拦截> * @Author Zhaiyt * @Date 16:18 2019/1/29 * @Param * @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor **/ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
shiro配置类编写主要需要注意以下几点:
1.Shiro 过滤器
2.SecurityManager 安全管理器
3.加密方式
4.域需要注入到SecurityManager中
4.自定义ShiroRealm编写:
package com.zxt.ms.configs; import com.zxt.ms.entity.UserInfo; import com.zxt.ms.service.IUserInfoService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.Set; /** * @ClassName ShiroRealm * @Description ms 梦想家 * @Author Zhai XiaoTao https://www.cnblogs.com/zhaiyt * @Date 2019/1/26 16:54 * @Version 1.0 */ @Slf4j @Component public class ShiroRealm extends AuthorizingRealm { @Autowired private IUserInfoService userInfoServiceImpl; /** * @Description <权限验证> * @Author Zhaiyt * @Date 14:57 2019/1/28 * @Param * @return org.apache.shiro.authz.AuthorizationInfo **/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //因为非正常退出,即没有显式调用 SecurityUtils.getSubject().logout() if (!SecurityUtils.getSubject().isAuthenticated()) { log.info("非正常退出,清除缓存"); doClearCache(principalCollection); SecurityUtils.getSubject().logout(); return null; } UserInfo userInfo = (UserInfo)principalCollection.getPrimaryPrincipal(); String username = userInfo.getUsername(); //用户存在 授权 if(StringUtils.isNotBlank(username)){ SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Set<String> roles=userInfoServiceImpl.findRoleByUser(userInfo.getUsername()); Set<String> permissions=userInfoServiceImpl.findPermissionByUser(userInfo.getUsername()); authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } return null; } /** * @Description <身份验证> * @Author Zhaiyt * @Date 14:58 2019/1/28 * @Param * @return org.apache.shiro.authc.AuthenticationInfo **/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("shiroRealm.doGetAuthenticationInfo() start ..."); //获取用户的输入的账号. String username = (String)authenticationToken.getPrincipal(); //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 if(StringUtils.isNotBlank(username)){ UserInfo userInfo = userInfoServiceImpl.findByUsername(username); if(userInfo == null){ log.error("用户不存在"); throw new UnknownAccountException("用户名或密码错误!"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userInfo, //用户名 userInfo.getPassword(), //密码 ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt getName() //realm name ); return authenticationInfo; } throw new UnknownAccountException("用户名或密码错误!"); } @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } /** * 清除权限缓存 * 使用方法:在需要清除用户权限的地方注入 ShiroRealm, * 然后调用其clearCache方法。 */ public void clearCache() { PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals(); super.clearCache(principals); } public static void main(String[] args) { String hashAlgorithmName = "MD5"; String credentials = "123456"; int hashIterations = 2; ByteSource credentialsSalt = ByteSource.Util.bytes("zhaizhai"); Object obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations); System.out.println(obj); } }
ShiroRealm 需要集成 AuthorizingRealm 这个里面要实现两个方法,一个认证 doGetAuthenticationInfo,一个授权 doGetAuthorizationInfo
5.测试controller编写
package com.zxt.ms.controller; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /** * @ClassName TestController * @Description ms 梦想家 * @Author Zhai XiaoTao https://www.cnblogs.com/zhaiyt * @Date 2019/1/28 16:16 * @Version 1.0 */ @Slf4j @Controller public class HomeController { @RequiresPermissions(value = "admin") @RequestMapping(value = "/index") public String test(){ return "index"; } @GetMapping(value = "/login") public String login(){ return "login"; } @RequestMapping("/loginUser") @ResponseBody public Map<String,Object> login(HttpServletRequest request, HttpServletResponse response) throws Exception{ log.info("HomeController.login()"); String username = request.getParameter("username"); String password = request.getParameter("password"); String rememberMe = request.getParameter("rememberMe"); Map<String,Object> map = new HashMap<>(); String ret=""; Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(rememberMe=="true"); map.put("code","FAILD"); try { currentUser.login(token); map.put("code","SUCCESS"); map.put("msg","登陆成功"); } catch (UnknownAccountException ex) { map.put("msg","账号错误"); log.error(MapUtils.getString(map,"msg")); } catch (IncorrectCredentialsException ex) { map.put("msg","密码错误"); log.error(MapUtils.getString(map,"msg")); } catch (LockedAccountException ex) { map.put("msg","账号已被锁定,请与管理员联系"); log.error(MapUtils.getString(map,"msg")); } catch (AuthenticationException ex) { map.put("msg","您没有授权"); log.error(MapUtils.getString(map,"msg")); } } return map; } }
代码基本上就上面那些,但是我深知,仅仅只有上面那些东西根本就跑不起来,我不知道我为什么要写这样的东西,可能某个小哥在看我写的博客时候也会骂我吧,写的什么鬼鸡仔。。。 我是没有办法把所有东西都整到这来,没有任何意义,有些坑必须自己去踩,只有踩过了,也许才会记得更清楚。
下面我要说坑了:
1.编写shiroFilter的时候我没有将静态数据过滤,导致页面展示格式错乱
2.登陆使用的是ajax,而shiro当做是表单的提交,所以一开始的 login controller 写的是有问题,导致一直报 302
3.认证无法通过,在shiro配置类中我指定了加密方式,为MD5 2次离散 ,在Realm中我指定了需要加 salt 的加密方式,因此密码的加密方式为 MD5 + salt 我使用main 跑出来加密后的数据,添加至数据库,可是一直无法认证成功,后发现在SecurityManager注入的ShiroRealm实体没有set加密方法...
4.认证成功后无法回调授权方法,原因,需要权限校验的方法上添加 @RequiresPermissions(value = "admin") OK