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
