Apache Shiro是一個功能強大、靈活的,開源的輕量級安全框架。它可以乾淨利落地處理身份驗證、授權、企業會話管理和加密,與spring security 相比較,簡單易用,靈活性高,springboot本身是提供了對security的支持,畢竟是自家的東西。springboot暫時沒有集成shiro,需要自己配置。
一.Shiro能做什麼呢?
(1)驗證用戶身份
(2)用戶訪問權限控制,比如:1、判斷用戶是否分配了一定的安全角色。2、判斷用戶是否被授予完成某個操作的權限
(3)在非 web 或 EJB 容器的環境下可以任意使用Session API
(4)可以響應認證、訪問控制,或者 Session 生命週期中發生的事件
(5)可將一個或以上用戶安全數據源數據組合成一個複合的用戶 “view”(視圖)
(6)支持單點登錄(SSO)功能
(7)支持提供“Remember Me”服務,獲取用戶關聯信息而無需登錄
…
等等——都集成到一個有凝聚力的易於使用的API。
二.Apache Shiro Features 特性
Apache Shiro是一個全面的、蘊含豐富功能的安全框架。下圖爲描述Shiro功能的框架圖:
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之爲應用安全的四大基石。那麼就讓我們來看看它們吧:
(1)Authentication(認證):用戶身份識別,通常被稱爲用戶“登錄”
(2)Authorization(授權):訪問控制。比如某個用戶是否具有某個操作的使用權限。
(3)Session Management(會話管理):特定於用戶的會話管理,甚至在非web 或 EJB 應用程序。
(4)Cryptography(加密):在對數據源使用加密算法加密的同時,保證易於使用。
三具體實現
管理都是層級相互依賴的,權限賦予給角色,而把角色又賦予用戶,這樣的權限設計很清楚,管理起來很方便。
1、pom.xml添加依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
2、編寫Shiro配置類
Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和權限校驗,所以我們需要定義一系列關於URL的規則和訪問權限。
package com.java.core.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
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 at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不設置值,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/");
// 設置無權限時跳轉的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/login.html");
// 設置攔截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/left", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
//開放登陸接口
filterChainDefinitionMap.put("/login", "anon");
// TODO
filterChainDefinitionMap.put("/test/test1", "anon");
//其餘接口一律攔截
//主要這行代碼必須放在所有權限設置的最後,不然會導致所有 url 都被攔截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro攔截器工廠類注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置realm.
securityManager.setRealm(customRealm());
//注入緩存管理器;
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
/**
* 自定義身份認證 realm;
* <p>
* 必須寫這個類,並加上 @Bean 註解,目的是注入 CustomRealm,
* 否則會影響 CustomRealm類 中其他類的依賴注入
*/
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
/**
* shiro緩存管理器;
* 需要注入對應的其它的實體類中:
* 1、安全管理器:securityManager
* 可見securityManager是整個shiro的核心;
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
/**
* thymeleaf和shiro
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
3、編寫自定義Realm驗證類,user表、user_role表和role_permissions表
package com.java.core.config;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
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.authc.UsernamePasswordToken;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import tk.mybatis.mapper.entity.Example;
import tk.mybatis.mapper.entity.Example.Criteria;
import com.java.system.dao.SystemUserMapper;
import com.java.system.model.SystemUser;
public class CustomRealm extends AuthorizingRealm {
@Autowired
private SystemUserMapper systemUserMapper;
/**
* 獲取身份驗證信息
* Shiro中,最終是通過 Realm 來獲取應用程序中的用戶、角色及權限信息的。
*
* @param authenticationToken 用戶身份信息 token
* @return 返回封裝了用戶信息的 AuthenticationInfo 實例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 身份認證
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
Example example = new Example(SystemUser.class);
Criteria criteria = example.createCriteria();
// 登錄名
criteria.andEqualTo("loginName", token.getUsername());
List<SystemUser> users = systemUserMapper.selectByExample(example);
String password = "";
if (users != null && users.size() > 0) {
password = users.get(0).getPassword();
}
if (users == null || users.size() == 0) {
throw new AccountException("用戶名不正確");
}
if (!password.equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密碼不正確");
}
if (users.get(0).getStatus() == null || users.get(0).getStatus() != 0) {
throw new AccountException("用戶已被禁用");
}
Set<String> permissions = systemUserMapper.queryPermissions(token.getUsername());
if (permissions == null || permissions.size() == 0) {
throw new AccountException("該用戶無任何權限");
}
// 把用戶信息放到session中
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
request.getSession().setAttribute("user", users.get(0));
// 在登陸的時候清楚一次緩存信息,這樣每次登陸重新獲取授權信息
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
// 若存在,將此用戶存放到登錄認證info中,無需自己做密碼對比,Shiro會爲我們進行密碼對比校驗
return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
}
/**
* 獲取授權信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 權限認證
String userName = (String) SecurityUtils.getSubject().getPrincipal();
Set<String> permissions = systemUserMapper.queryPermissions(userName);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permissions);
return info;
}
}
5、ehcache.xml的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<!--
name:緩存名稱。
maxElementsInMemory:緩存最大數目
maxElementsOnDisk:硬盤最大緩存個數。
eternal:對象是否永久有效,一但設置了,timeout將不起作用。
overflowToDisk:是否保存到磁盤,當系統當機時
timeToIdleSeconds:設置對象在失效前的允許閒置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閒置時間無窮大。
timeToLiveSeconds:設置對象在失效前允許存活時間(單位:秒)。最大時間介於創建時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0.,也就是對象存活時間無窮大。
diskPersistent:是否緩存虛擬機重啓期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每個Cache都應該有自己的一個緩衝區。
diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。
memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你可以設置爲FIFO(先進先出)或是LFU(較少使用)。
clearOnFlush:內存數量最大時是否清除。
memoryStoreEvictionPolicy:
Ehcache的三種清空策略;
FIFO,first in first out,這個是大家最熟的,先進先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。
LRU,Least Recently Used,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。
-->
<defaultCache eternal="false" maxElementsInMemory="1000"
overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />
<!-- 登錄記錄緩存鎖定10分鐘 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
6、以登錄身份驗證爲例,LoginController.java編寫
/**
* 登陸
*
* @param username 用戶名
* @param password 密碼
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public Object login(HttpServletRequest request, String loginName, String password) {
// 從SecurityUtils裏邊創建一個 subject
Subject subject = SecurityUtils.getSubject();
// 在認證提交前準備 token(令牌)
String username = loginName;
UsernamePasswordToken token = new UsernamePasswordToken(username, DigestUtils.md5Hex(password));
// 執行認證登陸
subject.login(token);
Map<String,Object> map = new HashMap<String,Object>();
map.put("status",200);
map.put("message","成功登陸");
return map;
}
7、前端頁面添加菜單權限管理,name -- 權限名
<shiro:hasAnyPermissions name="expend,recommend,recipe,moonKill,cookbook,carousel">
<ul class="borderBlue">
<p class="yyyy blue" ><span class="span1"></span>種花管理<span class="jiantou span2"></span></p>
<p class="slideP">
<span id="expend" class="slideInside" shiro:hasAnyPermissions="article,recommend" th:onclick="'javascript:go(\''+@{/test}+'\')'">種菜管理</span>
<span id="cookbook" class="slideInside" shiro:hasAnyPermissions="recipe,moonKill,cookbook" th:onclick="'javascript:go(\''+@{/test}+'\')'">買種管理</span>
<span id="carousel" class="slideInside" shiro:hasAnyPermissions="carousel" onclick="go('slideImage.html')">營銷管理</span>
</p>
</ul>
</shiro:hasPermission>