Shiro和Spring MVC、Mybatis整合教程

Apache Shiro 是Java的 安全框架 ,提供了認證(Authentication)、授權(Authorization)、會話(Session)管理、加密(Cryptography)等功能,且Shiro與Spring Security等安全框架相比具有簡單性、靈活性、支援細粒度鑑權、支援一級快取等, 還有Shiro不跟任何容器(Tomcat等)和框架(Sping等)捆綁,可以獨立執行,這也造就了Shiro不僅僅是可以用在Java EE上還可以用在Java SE上 。
Shiro四大功能
在開始之前,首先了解一下Shiro的四大功能,俗話說“知己知彼百戰不殆”。

認證
認證就是使用者訪問系統的時候,系統要驗證使用者身份的合法性,比如我們通常所說的“登入”就是認證的一種方式,只有登入成功了之後我們才能訪問相應的資源。在Shiro中,我們可以將使用者理解為 Subject 主體,在使用者身份認證的時候,使用者需要提供能證明他身份的資訊,如使用者名稱、密碼等,使用者所提供的這些使用者名稱、密碼則對應Shiro中的Principal、 Credentials,即在Subject進行身份認證的時候,需要提供相應的Principal、 Credentials,對應的程式碼如下:
UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(token); //提交認證 複製程式碼
我們知道Http協議是 無狀態 的,所以使用者認證成功後怎麼才能保持認證成功的狀態呢?如果是我們開發的話一般都是登入成功後將Session儲存在伺服器,然後再將Session返回給使用者,之後的請求使用者都將這個Session帶上,然後伺服器根據使用者請求攜帶的Session和伺服器儲存的Session進行比較來判斷使用者是否已認證。但是使用Shiro後, Shiro已經幫我們做好這個了(下面介紹的會話管理),是不是feel爽~
授權
授權可以理解為訪問控制,在使用者認證(登入)成功之後,系統對使用者訪問資源的許可權進行控制,即確定什麼使用者能訪問什麼資源,如普通使用者不能訪問後臺,但是管理員可以。在這裡我們還需要認識幾個概念,資源(Resource)、角色(Role)、許可權(Permission),上面提到的Subject主體可以有多個角色,每個角色又對應多個資源的多個許可權,這種 基於資源的訪問控制 可以實現細粒度的許可權。對主體設定角色、許可權的程式碼如下:
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //新增使用者的角色 authorizationInfo.addRoles(roleIdList); //新增使用者的許可權 authorizationInfo.addStringPermissions(resourceIdList); 複製程式碼
如果要實現這樣的授權功能,我們必定需要設計一個使用者組、許可權,給每個方法或者URL加上判斷,是否當前登入的使用者滿足條件。但是使用Shiro後, Shiro也幫我們幫這些都做好了 。
會話管理
會話管理的會話即Session,所謂會話,即使用者訪問應用時保持的連線關係,在多次互動中應用能夠識別出當前訪問的使用者是誰,且可以在多次互動中儲存一些資料。如訪問一些網站時登入成功後,網站可以記住使用者,且在退出之前都可以識別當前使用者是誰。在Shiro中,與使用者有關的一切資訊都可以通過Shiro的介面獲得,和使用者的會話Session也都由Shiro管理。如實現“記住我”或者“下次自動登入”的功能,如果要自己去開發的話,估計又得話不少時間。但是使用Shiro後, Shiro也幫我們幫這些都做好了 。
加密
使用者密碼明文儲存是不是安全,應不應該MD5加密,是不是應該加鹽,又要寫密碼加密的程式碼。 這些Shiro已經幫你做好了 。
Shiro三大核心概念
從整體概念上理解,Shiro的體系架構有三個主要的概念,Subject(主體),Security Manager (安全管理器)和 Realms (域)。

Subject主體
主體是當前正在操作的使用者的特定資料集合。主體可以是一個人,也可以代表第三方服務,守護程序,定時任務或類似的東西,也就是幾乎所有與該應用進行互動的事物。所有Subject都繫結到 SecurityManager
,與Subject的所有互動都會委託給 SecurityManager,可以把 Subject 認為是一個門面,SecurityManager 才是實際的執行者。
Security Manager安全管理器
安全管理器,即所有與安全有關的操作都會與 SecurityManager
互動,且它 管理著所有Subject 可以看出它是Shiro的核心,它負責與後邊介紹的其他元件進行互動,如果學習過 SpringMVC,你可以把它看成DispatcherServlet前端控制器, 一般來說,一個應用只會存在一個SecurityManager例項 。
Realms域
域,Shiro從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法,也需要從Realm得到使用者相應的角色 / 許可權進行驗證使用者是否能進行操作,即Realms作為Shiro與應用程式安全資料之間的“橋樑”。從這個意義上講,Realm實質上是一個安全相關的 DAO ,它封裝了資料來源的連線細節,並在需要時將相關資料提供給Shiro。其中Realm有2個方法, doGetAuthenticationInfo
用來認證, doGetAuthorizationInfo
用來授權。
Spring、Spring MVC、Mybatis、Shiro整合
專案目錄

新增依賴包
pox.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>shiro</groupId> <artifactId>shiro</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>shiro Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!--Sping核心依賴--> <!-- https://mvnrepository.com/artifact/org.springframework/spring-core --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-web --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.1.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.3.RELEASE</version> <scope>test</scope> </dependency> <!--Mybatis依賴--> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!--MySQL連線驅動--> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>shiro</finalName> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build> </project> 複製程式碼
建立資料庫和實體類
為了減少篇幅,只做簡單介紹,詳情可以檢視原始碼,資料庫檔案在本專案根目錄。

- resource表:資源表,有
id
,name
兩個欄位,分別對應資源id和許可權。 - role表:角色表,有
id
,name
兩個欄位,分別對應角色id和角色名。 - role_resource表:角色資源許可權表,有
id
,roleid
,resid
三個欄位,分別對應自增id、角色id和資源id。 - user表:使用者表,有
id
,username
,password
三個欄位,分別對應自增id、使用者名稱和密碼。 - user_role表:有
id
,uid
,rid
三個欄位,分別對應自增id、使用者id、和角色id。
Dao層
AccountDao.java:
public interface AccountDao { User findUserByUsername(String username); List<Role> findRoleByUserId(int id); List<Resource> findResourceByUserId(int id); } 複製程式碼
service層
AccountService.java:
public interface AccountService { User findUserByUsername(String username); List<Role> findRoleByUserId(int id); List<Resource> findResourceByUserId(int id); boolean login(User user); } 複製程式碼
AccountServiceImpl.java:
package com.shiro.service.impl; import com.shiro.dao.AccountDao; import com.shiro.entity.Role; import com.shiro.entity.User; import com.shiro.service.AccountService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; /** * @program: shiro * @description: * @author: Xue 8 * @create: 2019-02-01 15:37 **/ @Service public class AccountServiceImpl implements AccountService { @Resource AccountDao accountDao; /** * @description: 根據使用者名稱查詢使用者資訊 * @param: [username] * @return: com.shiro.entity.User * @author: Xue 8 * @date: 2019/2/1 */ @Override public User findUserByUsername(String username) { return accountDao.findUserByUsername(username); } @Override public List<Role> findRoleByUserId(int id) { return accountDao.findRoleByUserId(id); } @Override public List<com.shiro.entity.Resource> findResourceByUserId(int id) { return accountDao.findResourceByUserId(id); } public boolean login(User user){ //獲取當前使用者物件subject Subject subject = SecurityUtils.getSubject(); System.out.println("subject:" + subject.toString()); //建立使用者名稱/密碼身份證驗證Token UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); System.out.println("token" + token); try { subject.login(token); System.out.println("登入成功"); return true; } catch (Exception e) { System.out.println("登入失敗" + e); return false; } } } 複製程式碼
MyRealm.java
package com.shiro.service.impl; import com.shiro.entity.Role; import com.shiro.entity.User; import com.shiro.service.AccountService; 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.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * @program: shiro * @description: * @author: Xue 8 * @create: 2019-02-01 15:16 **/ public class MyRealm extends AuthorizingRealm { @Resource AccountService accountService; /** * 身份認證的方法 認證成功獲取身份驗證資訊 * 這裡最主要的是user.login(token);這裡有一個引數token,這個token就是使用者輸入的使用者密碼, * 我們平時可能會用一個物件user來封裝使用者名稱和密碼,shiro用的是token,這個是控制層的程式碼,還沒到shiro, * 當呼叫user.login(token)後,就交給shiro去處理了,接下shiro應該是去token中取出使用者名稱,然後根據使用者去查資料庫, * 把資料庫中的密碼查出來。這部分程式碼一般都是要求我們自定義實現,自定義一個realm,重寫doGetAuthenticationInfo方法 **/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //獲取使用者輸入的使用者名稱和密碼 //實際上這個token是從UserResource面currentUser.login(token)傳過來的 //兩個token的引用都是一樣的 String username = (String) authenticationToken.getPrincipal(); //密碼要用字元陣列來接受 因為UsernamePasswordToken(username, password) 儲存密碼的時候是將字串型別轉成字元陣列的 檢視原始碼可以看出 String password = new String((char[]) authenticationToken.getCredentials()); //呼叫service 根據使用者名稱查詢使用者資訊 User user = accountService.findUserByUsername(username); //String password = user.getPassword(); //判斷使用者是否存在 不存在則丟擲異常 if (user != null) { //判斷使用者密碼是否匹配 匹配則不匹配則丟擲異常 if (user.getPassword().equals(password)) { //登入成功 把使用者資訊儲存在Session中 Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("userSession", user); session.setAttribute("userSessionId", user.getId()); //認證成功 返回一個AuthenticationInfo的實現 return new SimpleAuthenticationInfo(username, password, getName()); } else { System.out.println("密碼不正確"); throw new IncorrectCredentialsException(); } } else { System.out.println("賬號不存在"); throw new UnknownAccountException(); } } /** * 授權的方法 * 1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去呼叫這個是否有什麼角色或者是否有什麼許可權的時候; * * 2、@RequiresRoles("admin") :在方法上加註解的時候; * * 3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在頁面上加shiro標籤的時候,即進這個頁面的時候掃描到有這個標籤的時候。 * 4、xml配置許可權的時候也會走 **/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("授權"); //從principalCollection獲取使用者資訊 //如果doGetAuthenticationInfo(user,password,getName()); 傳入的是user型別的資料 那這裡getPrimaryPrincipal獲取到的也是user型別的資料 String username = (String) principalCollection.getPrimaryPrincipal(); User user = accountService.findUserByUsername(username); //獲取該使用者的所有角色 List<Role> roleList = accountService.findRoleByUserId(user.getId()); //將角色的id放到一個String列表中 因為authorizationInfo.addRoles()方法只支援角色的String列表或者單個角色String List<String> roleIdList = new ArrayList<String>(); for (Role role:roleList) { roleIdList.add(role.getName()); } //獲取該使用者的所有許可權 List<com.shiro.entity.Resource> resourceList = accountService.findResourceByUserId(user.getId()); List<String> resourceIdList = new ArrayList<String>(); //將許可權id放到一個String列表中 因為authorizationInfo.addRoles()方法只支援角色的String列表或者單個角色String for (com.shiro.entity.Resource resource:resourceList) { resourceIdList.add(resource.getName()); } System.out.println("授權11"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //新增使用者的角色 authorizationInfo.addRoles(roleIdList); //新增使用者的許可權 authorizationInfo.addStringPermissions(resourceIdList); return authorizationInfo; } } 複製程式碼
controller層
AccountController.java
package com.shiro.controller; import com.shiro.entity.User; import com.shiro.service.AccountService; 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 javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; /** * @program: shiro * @description: * @author: Xue 8 * @create: 2019-02-01 13:14 **/ @Controller public class AccountController { @Resource AccountService accountService; @Resource HttpServletRequest servletRequest; @RequestMapping(value = "/home") public Stringhome(){ return "home"; } @RequestMapping(value = "/login", method = RequestMethod.GET) public StringgetLogin(){ return "login"; } @RequestMapping(value = "/login", method = RequestMethod.POST) public String doLogin(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password){ User user = new User(); user.setUsername(username); user.setPassword(password); if (accountService.login(user)) { return "/home"; } return "/login"; } } 複製程式碼
以 GET
方法訪問 /login
的時候,會出現登入頁面,輸入賬號密碼點選登入資料將以 POST
方式提交給 /login
,如果賬號密碼匹配返回 /home
的頁面,否則返回 /login
的頁面。 /home
頁面只有在登入且有許可權的情況下才可以訪問, 未登入情況下 訪問會轉跳 /login
頁面,這個在Shiro的配置檔案裡面配置。
配置檔案
applicationContext.xml:配置Spring
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--開啟掃描註冊--> <context:component-scan base-package="com.shiro"></context:component-scan> <!--讀取properties配置--> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:jdbcConfig.properties"></property> </bean> <!--配置資料來源--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${driverClassName}"></property> <property name="username" value="${username}"></property> <property name="password" value="${password}"></property> <property name="url" value="${url}"></property> </bean> <!--配置session工廠--> <bean id="sessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="configLocation" value="classpath:mybatis-config.xml"></property> <property name="mapperLocations" value="classpath:mapping/*.xml"></property> </bean> <!--配置掃描mapping--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.shiro.dao"></property> <property name="sqlSessionFactoryBeanName" value="sessionFactoryBean"></property> </bean> </beans> 複製程式碼
spring-shiro.xml:配置Shiro
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm"></property> </bean> <bean id="myRealm" class="com.shiro.service.impl.MyRealm"> <!--關閉許可權快取 不然doGetAuthorizationInfo授權方法不執行--> <property name="authorizationCachingEnabled" value="false"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"></property> <property name="successUrl" value="/success"></property> <!--登入頁面--> <property name="loginUrl" value="/login"></property> <property name="filterChainDefinitions"> <value> <!--配置`/home`只有擁有`admin`角色的使用者才可以訪問--> /home = authc,roles[admin] </value> </property> </bean> </beans> 複製程式碼
這裡需要注意的是 在配置Realm的時候,如果沒用上快取功能的話,需要將快取關掉,不然進不到doGetAuthorizationInfo授權方法。
測試
開啟 http://localhost:8080/login
登入頁面,填寫正確使用者名稱和密碼登入

登入成功 轉跳成功頁面

http://localhost:8080/home
頁面,自動轉跳到了
/login
登入頁面(即沒有許可權訪問),登入賬戶,再次開啟
http://localhost:8080/home
頁面即可正常訪問。