Springboot 整合shiro實現許可權控制的方法
Author:jeffrey
Date:2019-04-08
一、開發環境:
1、mysql - 5.7
2、navicat(mysql客戶端管理工具)
3、idea 2017.2
4、jdk8
5、tomcat 8.5
6、springboot2.1.3
7、mybatis 3
8、shiro1.4
9、maven3.3.9
二、資料庫設計
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-CB46ByC1-1604249108144)(img/shiro01.png)]
三、建立springboot專案
3.1 新增元件
新增 web、lombok、thymeleaf、jdbc、mysql、mybatis等模組;
3.2 pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <!--配置shiro依賴包--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!--配置資料庫連線池依賴--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.26</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.32</version> </dependency> <!--配置lombok外掛--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
3.3 建立專案包結構
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-NNuns5Pt-1604249108145)(img/shiro02.png)]
3.4 配置初始化檔案application.yml
#配置服務埠號 server: port: 8080 #配置資料來源 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/my_shiro?useUnicode=true&characterEncoding=utf-8 username: root password: 59852369 #配置mybatis mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.qf.domain.pojo
四、程式設計開發
4.1 實體類開發
SysUser.java
package com.qf.domain; import lombok.Data; import java.io.Serializable; import java.util.Date; /** * Created by 54110 on 2019-07-05. */ @Data public class SysUser implements Serializable { private int userId;//使用者id private String loginName;//登入名 private String password;// private Integer state; private Date createTime; private String realname; }
SysPermission.java
package com.qf.domain; import lombok.Data; import java.io.Serializable; /** * Created by 54110 on 2019-07-05. */ @Data public class SysPermission implements Serializable { private int permId; private String permName;//許可權名稱 private String permUrl;//許可權操作地址(路徑) private String menuName;//選單名 private String menuLevel;//選單級別(11:一級;12:二級。。。) private String menuCode;//選單編碼(每級兩位數字) private int ifValid; private String parentCode; }
4.2 資料訪問層介面開發
SysUserMapper.java
package com.qf.mapper; import com.qf.domain.SysUser; import org.apache.ibatis.annotations.Mapper; /** * Created by 54110 on 2019-07-05. */ @Mapper public interface SysUserMapper { public SysUser findUserByUsername(String username); }
SysPermissionMapper
package com.qf.mapper; import com.qf.domain.SysPermission; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * Created by 54110 on 2019-07-05. */ @Mapper public interface SysPermissionMapper { // 根據使用者登入名查詢其所擁有的許可權 public List<SysPermission> findPermissionsByLoginName(String loginName); }
4.3 Mybatis對映開發
SysUsersMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.qf.mapper.SysUserMapper"> <resultMap type="com.qf.domain.SysUser" id="userMap"> <id column="USERID" property="userid" /> <result column="LOGIN_NAME" property="loginName" /> <result column="PASSWORD" property="password" /> <result column="STATE" property="state" /> <result column="CREATE_TIME" property="createTime" /> <result column="REALNAME" property="realname" /> </resultMap> <sql id="tbusers_columns"> PASSWORD,LOGIN_NAME,CREATE_TIME,REALNAME,STATE </sql> <!--根據使用者名稱查詢物件 --> <select id="findUserByUsername" parameterType="string" resultMap="userMap"> SELECT <include refid="tbusers_columns"></include> FROM TB_SYS_USER US WHERE US.LOGIN_NAME = #{name} </select> </mapper>
SysPermissionMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.qf.mapper.SysPermissionMapper"> <resultMap type="com.qf.domain.SysPermission" id="permMap"> <id column="PERMISSION_ID" property="permId" /> <result column="PER_NAME" property="permName" /> <result column="MENU_URL" property="permUrl" /> <result column="MENU_NAME" property="menuName" /> <result column="MENU_TYPE" property="menuLevel" /> <result column="MENU_CODE" property="menuCode" /> <result column="PARENT_CODE" property="parentCode" /> <result column="IF_ViLID" property="ifValid" /> </resultMap> <select id="findPermissionsByLoginName" parameterType="string" resultMap="permMap"> SELECT p.* FROM TB_SYS_USER us,TB_USER_ROLE ur,TB_SYS_ROLE r,TB_ROLE_PERMISSION rp,TB_SYS_PERMISSION p WHERE us.USERID = ur.USER_ID AND ur.ROLE_ID = r.ROLE_ID AND r.ROLE_ID = rp.ROLE_ID AND rp.PERMISSION_ID = p.PERMISSION_ID AND trim(us.LOGIN_NAME) = #{loginName} ORDER BY p.MENU_CODE </select> </mapper>
4.4 業務層開發
SysUsersServiceImpl.java
package com.qf.service.impl; import com.qf.domain.SysUser; import com.qf.mapper.SysUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * Created by 54110 on 2019-07-05. */ @Service public class SysUsersServiceImpl { @Autowired private SysUserMapper userMapper; public SysUser queryUserByLoginName(String loginName) { SysUser tbUsers = userMapper.findUserByUsername(loginName); return tbUsers; } }
SysPermissionServiceImpl.java
package com.qf.service.impl; import com.qf.domain.SysPermission; import com.qf.mapper.SysPermissionMapper; import com.qf.service.SysPermissionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * Created by 54110 on 2019-07-05. */ @Service public class SysPermissionServiceImpl implements SysPermissionService { @Autowired private SysPermissionMapper permMapper; @Override public List<SysPermission> queryPermissionsByLoginName(String loginName) { List<SysPermission> list = permMapper.findPermissionsByLoginName(loginName); return list; } }
4.5 控制層介面開發
UserController.java
package com.qf.controller; import com.qf.service.SysUserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; 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.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import java.util.Map; /** * Created by 54110 on 2019-07-05. */ @Controller public class UserController { @Autowired private SysUserService userService; // 登入頁(view)展示 @RequestMapping("/login") public String showlogin(){ return "login"; } /** * 登入處理 * @param map 使用者登入表單資料 * @return 邏輯檢視 */ @RequestMapping(value="dealLogin",method= RequestMethod.POST) public String dealLogin(@RequestParam Map<String,Object> map){ System.out.println( map.values().toString()); try { Subject subject = SecurityUtils.getSubject();//從安全管理器中獲取主體物件 UsernamePasswordToken token = new UsernamePasswordToken();//構建令牌物件 token.setUsername(map.get("name").toString());//賦身份資訊 token.setPassword(map.get("password").toString().toCharArray());//賦憑證資訊 subject.login(token);//使用主體的login方法判定使用者的許可權 if(subject.isAuthenticated()){ // 已登陸 // 使用者資訊及許可權資訊的儲存(session|| redis) return "main"; } } catch (AuthenticationException e) { e.printStackTrace(); System.out.println("登入失敗"); } return "login"; } // 登入且擁有user: @RequestMapping("/one") public String showCaseOne(){ return "one"; } @RequestMapping("/two") public String showCaseTwo(){ return "two"; } // 許可權不足時,響應的頁面 @RequestMapping("/unauth") public String showPermission(){ return "unauth"; } // 使用者登出操作 @RequestMapping("/logout") public String logout(){ Subject subject = SecurityUtils.getSubject(); subject.logout();//登出 return "redirect:login"; } }
4.6 關於shiro的開發
a、自定義安全策略
MyShiroRealm.java
package com.qf.shiro; import com.qf.domain.SysPermission; import com.qf.domain.SysUser; import com.qf.service.SysPermissionService; import com.qf.service.SysUserService; 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.SimpleAuthenticationInfo; 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.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import java.util.Collection; import java.util.HashSet; import java.util.List; /** * Created by 54110 on 2019-07-05. */ public class MyShiroRealm extends AuthorizingRealm { @Autowired private SysUserService sysUserServiceImpl; @Autowired private SysPermissionService sysPermissionServiceImpl; private String username; // 系統授權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { Subject subject = SecurityUtils.getSubject();//獲取主體物件 String username =(String ) subject.getPrincipal();//獲取使用者身份資訊 List<SysPermission> permissions = sysPermissionService.queryPermissionsByLoginName(username);//根據使用者名稱獲取使用者的許可權資訊 // 許可權去重 Collection<String > perms = new HashSet<>(); for (SysPermission perm: permissions ) { perms.add(perm.getPermName()); } SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addStringPermissions(perms);//授權 return simpleAuthorizationInfo; } //使用者認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal();//獲取使用者資訊 //根據使用者資訊查詢資料庫獲取後端的使用者身份,轉交給securityManager判定 SysUser user1 = sysUserService.queryUserByLoginName(username);//從資料庫直接取 System.out.println(user1); if(user1!=null) { SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user1.getLoginName(),user1.getPassword(),getName()); return simpleAuthenticationInfo; } return null; } }
b、自定義Shiro配置管理
ShiroConfig.java
package com.qf.config; import com.qf.shiro.MyShiroRealm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * Created by 54110 on 2019-07-05. */ @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); Map<String,String> map = new HashMap<>(); map.put("/main","authc"); //必須登入才可訪問 map.put("/one","perms[user_edit]");//只有特定許可權(“user_edit”)的使用者登入後才可訪問 map.put("/two","perms[user_forbidden]");//只有特定許可權(“user_forbidden”)的使用者登入後才可訪問 shiroFilterFactoryBean.setLoginUrl("/login");//設定登入頁(匿名) shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");//許可權不足的錯誤提示頁 shiroFilterFactoryBean.setFilterChainDefinitionMap(map);//裝配攔截策略 return shiroFilterFactoryBean; } // 配置安全管理器(注入Realm物件) @Bean(name="defaultWebSecurityManager") public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(myShiroRealm); return defaultWebSecurityManager; } @Bean(name="myShiroRealm") //使用該註解是的Realm物件由spring容器管理 public MyShiroRealm myShiroRealm(){ MyShiroRealm shiroRealm = new MyShiroRealm(); return shiroRealm; } }
五、功能測試
位址列輸入http://localhost:8080/login
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-BdyHhsoT-1604249108147)(img\shiro03.png)]
使用使用者:admin密碼:admin登入,登入成功後顯示頁面如下:
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-LdoaSOGM-1604249108149)(img/shiro04.png)]
因為admin2使用者擁有case one功能的操作許可權,所以當滑鼠單擊case one連結時,顯示如下成功訪問頁面
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-Co1rUWJi-1604249108150)(img/shiro05.png)]
因為admin2沒有case two訪問許可權,當用戶單擊case two時,會顯示無許可權訪問的頁面:
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-geha4PCi-1604249108150)(img/shiro06.png)]
當單擊logout連結時,系統重回登入頁。此時使用使用者test2密碼test2再次登入。因test2使用者無case one許可權,有case two許可權,所以當test2使用者單擊case two時會顯示如下頁面:
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-AWZrOeGj-1604249108151)(img/shiro07.png)]
六、啟動shiro註解模式
6.1、在ShiroConfig.java中註釋掉原先使用路徑過濾的許可權攔截語句:
//只有特定許可權(“user_edit”)的使用者登入後才可訪問 // map.put("/one","perms[user_edit]"); //只有特定許可權(“user_forbidden”)的使用者登入後才可訪問 // map.put("/two","perms[user_forbidden]");
6.2、修改ShiroConfig.java類程式碼新增如下內容:
/** * 開啟Shiro註解(如@RequiresRoles,@RequiresPermissions),* 需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor) */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** * 開啟aop註解支援 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager); return authorizationAttributeSourceAdvisor; }
6.3、修改UserController.java控制器介面程式碼:
// 登入且擁有user: @RequiresPermissions(value={"user_edit"}) @RequestMapping("/one") public String showCaseOne(){ return "one"; } @RequiresPermissions(value={"user_forbidden"}) @RequestMapping("/two") public String showCaseTwo(){ return "two"; }
6.4、測試
此事有許可權訪問的也頁面正常,但未授權的頁面,無法進入提示頁,顯示如下:
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-pM9lRu2Z-1604249108152)(img/shiro08.png)]
後臺亦丟擲org.apache.shiro.authz.AuthorizationException異常:
[外鏈圖片轉存失敗,建議將圖片儲存下來直接上傳(img-k8KTUuoi-1604249108152)(img/shiro09.png)]
6.5、此時使用aop攔截丟擲的異常
package com.jeffrey.exception; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; /** * Created by jeffrey on 2019/4/8. */ @ControllerAdvice public class ExceptionController { @ExceptionHandler(value = UnauthorizedException.class)//處理訪問方法時許可權不足問題 public String defaultErrorHandler(HttpServletRequest req,Exception e) { return "unauth"; } }
七、shiro密碼的MD5加密處理
7.1.密碼的加密
在資料表中存的密碼不應該是12345,而應該是12345加密之後的字串,而且還要求這個加密演算法是不可逆的,即由加密後的字串不能反推回來原來的密碼,如果能反推回來那這個加密是沒有意義的。
著名的加密演算法,比如 MD5,SHA1
7.2.MD5加密
1). 如何把一個字串加密為MD5
2). 使用MD5加密演算法後,前臺使用者輸入的字串如何使用MD5加密,需要做的是將當前的Realm 的credentialsMatcher屬性,替換為Md5CredentialsMatcher 由於Md5CredentialsMatcher已經過期了,推薦使用HashedCredentialsMatcher 並設定加密演算法即可。
7.3.使用MD5加密
1). 修改ShiroConfig.java檔案新增如下內容;
/** * 密碼校驗規則HashedCredentialsMatcher * 這個類是為了對密碼進行編碼的,* 防止密碼在資料庫裡明碼儲存,當然在登陸認證的時候,* 這個類也負責對form裡輸入的密碼進行編碼 * 處理認證匹配處理器:如果自定義需要實現繼承HashedCredentialsMatcher */ @Bean("hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //指定加密方式為MD5 credentialsMatcher.setHashAlgorithmName("MD5"); //加密次數 credentialsMatcher.setHashIterations(1024); credentialsMatcher.setStoredCredentialsHexEncoded(true); return credentialsMatcher; } @Bean("myShiroRealm") public MyShiroRealm myShiroRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) { MyShiroRealm authRealm = new MyShiroRealm(); authRealm.setAuthorizationCachingEnabled(false); authRealm.setCredentialsMatcher(matcher); return authRealm; }
2).修改MyRealm.java的認證邏輯如下:
//使用者認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal();//獲取使用者資訊 SysUser user1 = sysUserService.queryUserByLoginName(username);//從資料庫直接取 System.out.println(user1); if(user1!=null) { //當前realm物件的name String realmName = getName(); //鹽值 ByteSource credentialsSalt = ByteSource.Util.bytes(username); //封裝使用者資訊,構建AuthenticationInfo物件並返回 AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(username,credentialsSalt,realmName); return authcInfo; } return null; }
3). 通過new SimpleHash(hashAlgorithmName,credentials,salt,hashIterations); 我們可以得到"12345"經過MD5 加密1024後的字串;
package com.jeffrey; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.util.ByteSource; /** * Created by jeffrey on 2019/4/8. */ public class MD5Salt { public static void main(String[] args){ String hashAlgorithName = "MD5";//加密演算法 String password = "12345";//登陸時的密碼 int hashIterations =1024;//加密次數 ByteSource credentialsSalt = ByteSource.Util.bytes("admin2");//使用登入名做為salt SimpleHash simpleHash = new SimpleHash(hashAlgorithName,password,hashIterations); System.out.println("ok "+simpleHash); } }
使用密文替換資料庫中的明文密碼;
7.4 測試 略。。。。
7.5 後記 為什麼使用 MD5 鹽值加密: 希望即使兩個原始密碼相同,加密得到的兩個字串也不同。
為什麼使用 MD5 鹽值加密:
希望即使兩個原始密碼相同,加密得到的兩個字串也不同。
如何做到:
1). 在 doGetAuthenticationInfo 方法返回值建立 SimpleAuthenticationInfo 物件的時候,需要使用SimpleAuthenticationInfo(principal,realmName) 構造器
2). 使用 ByteSource.Util.bytes() 來計算鹽值.
3). 鹽值需要唯一: 一般使用隨機字串或 user id
4). 使用 new SimpleHash(hashAlgorithmName,hashIterations); 來計算鹽值加密後的密碼的值.
到此這篇關於Springboot 整合shiro實現許可權控制的文章就介紹到這了,更多相關Springboot許可權控制內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!