前言
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 String home(){
return "home";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String getLogin(){
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
頁面即可正常訪問。
總結
這是我學習Shiro時候根據自己的情況記錄下來的,希望對大家有所幫助,如果大家想對Shiro進一步研究的話,推薦大家看張開濤老師的《跟我學Shiro》,最後附上本專案的Github地址:github.com/xue8/Java-D…
原文地址:ddnd.cn/2019/02/02/…