Shiro提供身份驗證、授權、企業會話管理和加密等功能。
1、添加依賴:
<!-- shiro spring. -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<!-- shiro-thymeleaf 2.0.0-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2、在src/main/resources添加config文件夾,創建ehcache-shiro.xml文件,用於權限緩存:
<?xml version="1.0" encoding="UTF-8"?>
<!--<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"-->
<!--xsi:noNamespaceSchemaLocation="ehcache.xsd">-->
<ehcache name="es">
<diskStore path="java.io.tmpdir"/>
<!--
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
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120" />
<cache
name="user"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"/>
<!-- 登錄記錄緩存鎖定1小時 -->
<cache
name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true" />
</ehcache>
3、實現自定義的ShiroRealm.java類:
package com.example.demo.realm;
import com.example.demo.entity.Menu;
import com.example.demo.entity.Role;
import com.example.demo.entity.User;
import com.example.demo.service.MenuService;
import com.example.demo.service.RoleService;
import com.example.demo.service.UserService;
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.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 在認證、授權內部實現機制中都有提到,最終處理都將交給Real進行處理。因為在Shiro中,最終是通過Realm來獲取應用程序中的用戶、角色及權限信息的。通常情況下,在Realm中會直接從我們的數據源中獲取Shiro需要的驗證信息。可以說,Realm是專用於安全框架的DAO.
* Shiro的認證過程最終會交由Realm執行,這時會調用Realm的getAuthenticationInfo(token)方法。
* 該方法主要執行以下操作:
*
* 檢查提交的進行認證的令牌信息
* 根據令牌信息從數據源(通常為數據庫)中獲取用戶信息
* 對用戶信息進行匹配驗證。
* 驗證通過將返回一個封裝了用戶信息的AuthenticationInfo實例。
* 驗證失敗則拋出AuthenticationException異常信息。而在我們的應用程序中要做的就是自定義一個Realm類,繼承AuthorizingRealm抽象類,重載doGetAuthenticationInfo(),重寫獲取用戶信息的方法。
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
/**
* 驗證用戶身份
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
//實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鍾內不會重復執行該方法
User user = this.userService.findByName(userName);
//這里校驗了,CredentialsMatcher就不需要了,如果這里不校驗,調用CredentialsMatcher校驗
if (user == null) {
throw new UnknownAccountException("用戶名或密碼錯誤!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用戶名或密碼錯誤!");
}
if ("0".equals(user.getEnabled())) {
throw new LockedAccountException("賬號已被鎖定,請聯系管理員!");
}
//也可以在此處更新最后登錄時間(或在登錄方法實現)
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
// SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()),getName()); ////salt=username+salt
return info;
}
/**
* 授權用戶權限
* 授權的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>標簽的時候調用的,它會去檢測shiro框架中的權限(這里的permissions)是否包含有該標簽的name值,如果有,里面的內容顯示,如果沒有,里面的內容不予顯示(這就完成了對於權限的認證.)
*/
/**
* shiro的權限授權是通過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();
* 當訪問到頁面的時候,鏈接配置了相應的權限或者shiro標簽才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有權限的控制的話,那么這個方法可以不進行實現,直接返回null即可。
* 在這個方法中主要是使用類:SimpleAuthorizationInfo
* 進行角色的添加和權限的添加。
* authorizationInfo.addRole(role.getRole());
* authorizationInfo.addStringPermission(p.getPermission());
* 當然也可以添加set集合:roles是從數據庫查詢的當前用戶的角色,stringPermissions是從數據庫查詢的當前用戶對應的權限
* authorizationInfo.setRoles(roles);
* authorizationInfo.setStringPermissions(stringPermissions);
* 就是說如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權限添加]”);
* 就說明訪問/add這個鏈接必須要有“權限添加”這個權限才可以訪問,
* 如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權限添加]”);
* 就說明訪問/add這個鏈接必須要有“權限添加”這個權限和具有“100002”這個角色才可以訪問。
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//獲取用戶
User user = (User) SecurityUtils.getSubject().getPrincipal();
// User user = (User) principalCollection.getPrimaryPrincipal();
// User user=(User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();//獲取session中的用戶
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Role> roles=this.roleService.findRolesByUserId(user.getId());
//獲取用戶角色
Set<String> roleSet = new HashSet<String>();
for (Role role:roles) {
roleSet.add(role.getName());
}
info.setRoles(roleSet);
List<Menu> menus=this.menuService.findMenusByUserId(user.getId());
//獲取用戶權限
Set<String> permissionSet = new HashSet<String>();
for (Menu menu:menus) {
if(!StringUtils.isEmpty(menu.getPermission())) { //權限為空會異常,Caused by: java.lang.IllegalArgumentException: Wildcard string cannot be null
CollectionUtils.mergeArrayIntoCollection(menu.getPermission().split(","), permissionSet);
}
}
info.setStringPermissions(permissionSet);
return info;
}
/**
* 待補充:
* shiro+redis集成,避免每次訪問有權限的鏈接都會去執行MyShiroRealm.doGetAuthenticationInfo()方法來查詢當前用戶的權限,因為實際情況中權限是不會經常變得,這樣就可以使用redis進行權限的緩存。
* 實現shiro鏈接權限的動態加載,之前要添加一個鏈接的權限,要在shiro的配置文件中添加filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權限添加]”),這樣很不方便管理,一種方法是將權限使用數據庫進行加載,另一種是通過init配置文件的方式讀取。
* Shiro 自定義權限校驗Filter定義,及功能實現。
* Shiro Ajax請求權限不滿足,攔截后解決方案。這里有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉,所以Ajax請求假如沒登錄,那么這個請求給用戶的感覺就是沒有任何反應,而用戶又不知道用戶已經退出了。
* 在線顯示,在線用戶管理(踢出登錄)。
* 登錄注冊密碼加密傳輸。
* 記住我的功能。關閉瀏覽器后還是登錄狀態。
*/
}
4、如有需要實現自定義的密碼校驗CredentialsMatcher.java:
package com.example.demo.realm;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
/**
* shiro中唯一需要程序員編寫的兩個類:類ShiroRealm完成根據用戶名去數據庫的查詢,並且將用戶信息放入shiro中,供第二個類調用.CredentialsMatcher,完成對於密碼的校驗.其中用戶的信息來自ShiroRealm類
*/
public class CredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken utoken=(UsernamePasswordToken) token;
//獲得用戶輸入的密碼:(可以采用加鹽(salt)的方式去檢驗)
String inPassword = new String(utoken.getPassword());
//獲得數據庫中的密碼
String dbPassword=(String) info.getCredentials();
//進行密碼的比對
return this.equals(inPassword, dbPassword);
}
}
5、根據需要,選擇創建ShiroSessionListener.java:
package com.example.demo.listener;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
public class ShiroSessionListener implements SessionListener{
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
}
6、根據需要創建自定義Filter:
package com.example.demo.filter;
import java.io.IOException;
import java.util.Set;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @Type MyFilter.java
* @Desc 用於自定義過濾器,過濾用戶請求是否被授權 ,MyFilter是用於過濾需要權限校驗的請求
*/
public class MyFilter extends AuthorizationFilter {
@SuppressWarnings("unchecked")
@Override
protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object arg2) throws Exception {
HttpServletRequest request = (HttpServletRequest) req;
//獲取請求路徑
String path = request.getServletPath();
Subject subject = getSubject(req, resp);
if (null != subject.getPrincipals()) {
//根據session中存放的用戶權限,比對路徑,如果擁有該權限則放行
Set<String> userPrivileges = (Set<String>) request.getSession()
.getAttribute("USER_PRIVILEGES");
if (null != userPrivileges && userPrivileges.contains(path)) {
return true;
}
}
return false;
}
/**
* 會話超時或權限校驗未通過的,統一返回401,由前端頁面彈窗提示
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
if (isAjax((HttpServletRequest) request)) {
WebUtils.toHttp(response).sendError(401);
} else {
String unauthorizedUrl = getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(401);
}
}
return false;
}
private boolean isAjax(HttpServletRequest request) {
String header = request.getHeader("x-requested-with");
if (null != header && "XMLHttpRequest".endsWith(header)) {
return true;
}
return false;
}
}
package com.example.demo.filter;
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
/**
* @Type LoginFilter.java
* @Desc 用於自定義過濾器,過濾用戶請求時是否是登錄狀態 loginFilter主要是覆蓋了自帶的authc過濾器,讓未登錄的請求統一返回401
*/
public class LoginFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object arg2) throws Exception {
Subject subject = getSubject(req, resp);
if (null != subject.getPrincipals()) {
return true;
}
return false;
}
/**
* 會話超時或權限校驗未通過的,統一返回401,由前端頁面彈窗提示
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
if (isAjax((HttpServletRequest) request)) {
WebUtils.toHttp(response).sendError(401);
} else {
String unauthorizedUrl = getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(401);
}
}
return false;
}
private boolean isAjax(HttpServletRequest request) {
String header = request.getHeader("x-requested-with");
if (null != header && "XMLHttpRequest".endsWith(header)) {
return true;
}
return false;
}
}
6、創建Shiro配置類,可用shiro-conf.xml代替:
package com.example.demo.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.example.demo.listener.ShiroSessionListener;
import com.example.demo.realm.CredentialsMatcher;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import com.example.demo.realm.ShiroRealm;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.ValidatingSessionManager;
import org.apache.shiro.session.mgt.eis.*;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
//import org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler
/**
* shiro的配置類,需要注意一點filterChainDefinitionMap必須是LinkedHashMap因為它必須保證有序
* @author Administrator
*
*/
@Configuration
public class ShiroConfig {
//授權緩存管理器
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return em;
}
/**
* 使用上面的Ehcache或下面的shiro自帶的內存緩存實現
*/
// @Bean
// public MemoryConstrainedCacheManager getMemoryConstrainedCacheManager() {
// return new MemoryConstrainedCacheManager();
// }
/**
* ShiroFilterFactoryBean 處理攔截資源文件問題。
* 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,因為在
* 初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager
*
* Filter Chain定義說明 1、一個URL可以配置多個Filter,使用逗號分隔 2、當設置多個過濾器時,全部驗證通過,才視為通過
* 3、部分過濾器可指定參數,如perms,roles
*
* <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
* <property name="securityManager" ref="securityManager" />
* <!-- 配置登錄頁 -->
* <property name="loginUrl" value="/login.jsp" />
* <!-- 配置登錄成功后的頁面 -->
* <property name="successUrl" value="/list.jsp" />
* <property name="unauthorizedUrl" value="/unauthorized.jsp" />
* <property name="filterChainDefinitions">
* <value>
* <!-- 靜態資源允許訪問 -->
* <!-- 登錄頁允許訪問 -->
* /login.jsp = anon
* /test/login = anon
* /user/delete = perms["delete"]
* /logout = logout
* <!-- 其他資源都需要認證 -->
* /** = authc
* </value>
* </property>
* </bean>
*/
/**
* Shiro主過濾器本身功能十分強大,其強大之處就在於它支持任何基於URL路徑表達式的、自定義的過濾器的執行
* Web應用中,Shiro可控制的Web請求必須經過Shiro主過濾器的攔截,Shiro對基於Spring的Web應用提供了完美的支持
* @param securityManager
* @return
*/
@Bean(name="shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設置 SecurityManager,Shiro的核心安全接口
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置登錄的url,如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面(源碼)
shiroFilterFactoryBean.setLoginUrl("/login"); //這是后台的/控制器
// 登錄成功后要跳轉的鏈接,本例中此屬性用不到,因為登錄成功后的處理邏輯在LoginController里硬編碼了
shiroFilterFactoryBean.setSuccessUrl("/index"); //這是Index.html頁面
// 未授權界面;配置不會被攔截的鏈接 順序判斷
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); ////這里設置403並不會起作用
// 有自定義攔截器就放開 wangzs(源碼)
// //自定義攔截器
// LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
// //限制同一帳號同時在線的個數。
// //filtersMap.put("kickout", kickoutSessionControlFilter());
// shiroFilterFactoryBean.setFilters(filtersMap);
// 配置訪問權限,權限控制map.Shiro連接約束配置,即過濾鏈的定義,
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不會被攔截的鏈接 順序判斷
// value值的'/'代表的路徑是相對於HttpServletRequest.getContextPath()的值來的
// anon:它對應的過濾器里面是空的,什么都沒做,這里.do和.jsp后面的*表示參數,比方說login.jsp?main這種
// authc:該過濾器下的頁面必須驗證后才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
// 配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了
// filterChainDefinitionMap.put("/logout", "logout");
// 從數據庫獲取動態的權限
// filterChainDefinitionMap.put("/add", "perms[權限添加]"); /userList=roles[admin],需要有admin這個角色,如果沒有此角色訪問此URL會返回無授權頁面,或authc,perms[user:list]
// <!-- 需要權限為add的用戶才能訪問此請求-->
// /user=perms[user:add]
// <!-- 需要管理員角色才能訪問此頁面 -->
// /user/add=roles[admin]或roles[admin],perms[user:add]
// <!-- 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
// <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
//logout這個攔截器是shiro已經實現好了的。
// 從數據庫獲取
/*List<SysPermissionInit> list = sysPermissionInitService.selectAll();
for (SysPermissionInit sysPermissionInit : list) {
filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
sysPermissionInit.getPermissionInit());
}*/
/**
* 可自定義過濾器,比如myFilter替代authc
* <bean id="myFilter" class="com.cmcc.hygcc.comm.shiro.MyFilter"></bean>
* <property name="filters">
* <map>
* <entry key="myFilter" value-ref="myFilter" />
* <!-- 覆蓋authc過濾器,使得未登錄的ajax請求返回401狀態 -->
* <entry key="authc" value-ref="loginFilter" />
* </map>
* </property>
*
* /**=myFilter
*/
//靜態資源允許訪問//登錄頁允許訪問,一個URL可以配置多個Filter,使用逗號分隔,當設置多個過濾器時,全部驗證通過,才視為通過,部分過濾器可指定參數,如perms,roles
// filterChainDefinitionMap.put("/login.html*", "anon"); //表示可以匿名訪問,*表示參數如?error等
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/unauthorized", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/user/regist", "anon");
filterChainDefinitionMap.put("/gifCode", "anon");
filterChainDefinitionMap.put("/logout", "logout"); //logout是shiro提供的過濾器
filterChainDefinitionMap.put("/user/delete", "perms[\"user:delete\"]"); //此時訪問/user/delete需要delete權限,在自定義Realm中為用戶授權。
//其他資源都需要認證
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean; //Shiro攔截器工廠類注入成功
}
//配置核心安全事務管理器
@Bean(name="securityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設置realm.
securityManager.setRealm(shiroRealm()); //如果方法加上參數@Qualifier("shiroRealm") ShiroRealm shiroRealm,可以直接securityManager.setRealm(shiroRealm);
//注入記住我管理器;
securityManager.setRememberMeManager(rememberMeManager());
// 自定義緩存實現 可使用redis
// securityManager.setCacheManager(cacheManager());
securityManager.setCacheManager(getEhCacheManager()); //緩存管理器
// 自定義session管理 可使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
//Shiro生命周期處理器
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//配置自定義的權限登錄器,本段不需要配置自定義的密碼比較器,可以換成下面的~~里面的寫法,需要創建自定義的密碼比較器CredentialsMatcher.java
@Bean //必須,身份認證realm; (這個需要自己寫,賬號密碼校驗;權限等)
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
//需要這種方式就放開
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// //配置自定義的權限登錄器
// @Bean(name="shiroRealm")
// public ShiroRealm shiroRealm(@Qualifier("credentialsMatcher") CredentialsMatcher matcher) {
// ShiroRealm shiroRealm=new ShiroRealm();
// shiroRealm.setCredentialsMatcher(matcher);
// return shiroRealm;
// }
// //配置自定義的密碼比較器
// @Bean(name="credentialsMatcher")
// public CredentialsMatcher credentialsMatcher() {
// return new CredentialsMatcher();
// }
// //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
* shiro的密碼比較器
* @return
*/
// @Bean
// public HashedCredentialsMatcher hashedCredentialsMatcher() {
// HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;
// hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5(""));
// return hashedCredentialsMatcher;
// }
/** //必須
* cookie對象;會話Cookie模板 ,默認為: JSESSIONID 問題: 與SERVLET容器名沖突,重新定義為sid或rememberMe,自定義
* @return
*/
public SimpleCookie rememberMeCookie() {
//這個參數是cookie的名稱,對應前端的checkbox的name = rememberMe
SimpleCookie cookie = new SimpleCookie("rememberMe");
//<!-- 記住我cookie生效時間30天 ,單位秒;-->
cookie.setHttpOnly(true);
cookie.setMaxAge(86400);
return cookie;
}
/** //必須
* cookie管理對象;記住我功能,rememberMe管理器
* @return
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位) //3AvVhmFLUs0KTA3Kprsdag==
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
/**
* 使用shiro注解為用戶授權 1. 在shiro-config.xml開啟shiro注解(硬編碼,修改權限碼很麻煩)
* 在方法上配置注解@RequiresPermissions("xxx:yyy")
* <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
* <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
* <property name="securityManager" ref="securityManager"/>
* </bean>
* 編程方式實現用戶權限控制
* Subject subject = SecurityUtils.getSubject();
* if(subject.hasRole("admin")){
* //有權限
* }else{
* //無權限
* }
* @return AOP式方法級權限檢查,DefaultAdvisorAutoProxyCreator用來掃描上下文,尋找所有的Advistor(通知器),將這些Advisor應用到所有符合切入點的Bean中。
* LifecycleBeanPostProcessor將Initializable和Destroyable的實現類統一在其內部自動分別調用了Initializable.init()和Destroyable.destroy()方法,從而達到管理shiro bean
* 生命周期的目的。
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
// AOP式方法級權限檢查
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { //@Qualifier("securityManager") SecurityManager manager
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean //必須(thymeleaf頁面使用shiro標簽控制按鈕是否顯示) //未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
//SessionManager和SessionDAO可以不配置,會話DAO
@Bean
public SessionDAO sessionDAO() {
MemorySessionDAO sessionDAO = new MemorySessionDAO();
return sessionDAO;
}
/**
* sessionDao的方法2
* @return
*/
// @Bean //
// public SessionIdGenerator sessionIdGenerator() {
// return new JavaUuidSessionIdGenerator();
// }
// @Bean
// public SessionDAO sessionDAO() {
// EnterpriseCacheSessionDAO cacheSessionDAO=new EnterpriseCacheSessionDAO();
// cacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
// cacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
// return cacheSessionDAO;
// }
//// 會話管理器,設定會話超時及保存
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
listeners.add(new ShiroSessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setGlobalSessionTimeout(1800000); //全局會話超時時間(單位毫秒),默認30分鍾
sessionManager.setSessionDAO(sessionDAO());
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionValidationSchedulerEnabled(true);
//定時清理失效會話, 清理用戶直接關閉瀏覽器造成的孤立會話
sessionManager.setSessionValidationInterval(1800000);
// sessionManager.setSessionValidationScheduler(executorServiceSessionValidationScheduler());
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(rememberMeCookie());
return sessionManager;
}
//會話驗證調度器,每30分鍾執行一次驗證
@Bean(name="sessionValidationScheduler")
public ExecutorServiceSessionValidationScheduler executorServiceSessionValidationScheduler() {
ExecutorServiceSessionValidationScheduler sessionValidationScheduler=new ExecutorServiceSessionValidationScheduler();
sessionValidationScheduler.setInterval(1800000);
sessionValidationScheduler.setSessionManager((ValidatingSessionManager)sessionManager());
return sessionValidationScheduler;
}
}
7、未授權的設置不生效,需要添加未授權異常處理:
package com.example.demo.resolver;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import java.util.Properties;
/**
* 定制的異常處理類
*/
// private void applyUnauthorizedUrlIfNecessary(Filter filter) {
// String unauthorizedUrl = getUnauthorizedUrl();
// if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) {
// AuthorizationFilter authzFilter = (AuthorizationFilter) filter;
// //only apply the unauthorizedUrl if they haven't explicitly configured one already:
// String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl();
// if (existingUnauthorizedUrl == null) {
// authzFilter.setUnauthorizedUrl(unauthorizedUrl);
// }
// }
// }
//shiro默認過濾器(10個)
// anon -- org.apache.shiro.web.filter.authc.AnonymousFilter
// authc -- org.apache.shiro.web.filter.authc.FormAuthenticationFilter
// authcBasic -- org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
// perms -- org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
// port -- org.apache.shiro.web.filter.authz.PortFilter
// rest -- org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
// roles -- org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
// ssl -- org.apache.shiro.web.filter.authz.SslFilter
// user -- org.apache.shiro.web.filter.authc.UserFilter
// logout -- org.apache.shiro.web.filter.authc.LogoutFilter
@Configuration
public class DzExceptionResolver {
/**
* shiro中unauthorizedUrl不起作用,這是因為shiro源代碼private void applyUnauthorizedUrlIfNecessary(Filter filter)中判斷了filter是否為AuthorizationFilter,只有perms,roles,ssl,rest,port才是屬於AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl設置后不起作用。
* 解決方法:在shiro配置文件中添加(異常全路徑做key,錯誤頁面做value)
* <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
* <property name="exceptionMappings">
* <props>
* <prop key="org.apache.shiro.authz.UnauthorizedException">/403</prop>
* </props>
* </property>
* </bean>
*/
@Bean
public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
Properties properties=new Properties();
properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}
/**
* 相當於調用SecurityUtils.setSecurityManager(securityManager)
* @param securityManager
* @return
*/
@Bean
public MethodInvokingFactoryBean getMethodInvokingFactoryBean(@Qualifier("securityManager")SecurityManager securityManager) {
MethodInvokingFactoryBean methodInvokingFactoryBean=new MethodInvokingFactoryBean();
methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
methodInvokingFactoryBean.setArguments(securityManager);
return methodInvokingFactoryBean;
}
}
8、登錄和退出Controller:
package com.example.demo.controller;
import com.example.demo.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Properties;
@Controller
public class LoginController {
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String login() {
return "login";
}
/**
* 和shiro框架的交互完全通過Subject這個類去交互,用它完成登錄,注銷,獲取當前的用戶對象等操作
* login請求調用subject.login之后,shiro會將token傳遞給自定義realm,此時realm會先調用doGetAuthenticationInfo(AuthenticationToken authcToken )登錄驗證的方法,驗證通過后會接着調用 doGetAuthorizationInfo(PrincipalCollection principals)獲取角色和權限的方法(授權),最后返回視圖。
* 當其他請求進入shiro時,shiro會調用doGetAuthorizationInfo(PrincipalCollection principals)去獲取授權信息,若是沒有權限或角色,會跳轉到未授權頁面,若有權限或角色,shiro會放行,此時進入真正的請求方法……
* @param username
* @param password
* @param model
* @param session
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
// public String loginUser(String username,String password,boolean remeberMe,HttpSession session) {
public String loginUser(HttpServletRequest request, String username, String password, Model model, HttpSession session) {
// password=new SimpleHash("md5", password, ByteSource.Util.bytes(username.toLowerCase() + "shiro"),2).toHex();
// UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password,remeberMe);
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(usernamePasswordToken); //完成登錄
User user=(User) subject.getPrincipal();
//更新用戶登錄時間,也可以在ShiroRealm里面做
session.setAttribute("user", user);
model.addAttribute("user",user);
return "index";
} catch(Exception e) {
String exception = (String) request.getAttribute("shiroLoginFailure");
//logger.info("登錄失敗從request中獲取shiro處理的異常信息,shiroLoginFailure:就是shiro異常類的全類名");
model.addAttribute("msg",e.getMessage());
return "login";//返回登錄頁面
}
}
@RequestMapping("/logout")
public String logout(HttpSession session,Model model) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
// session.removeAttribute("user");
model.addAttribute("msg","安全退出!");
return "login";
}
}
9、權限測試Controller:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@Autowired
private UserService userService;
@RequestMapping("/index")
public User index() {
return userService.getUserById(1);
}
@RequiresPermissions("test")
@RequestMapping("/test")
public String test() {
return "ok";
}
}
10、項目用的Thymeleaf,要在頁面使用shiro標簽,首先添加依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- shiro-thymeleaf 2.0.0-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
11、application.properties配置thymeleaf:
#thymeleaf的配置是去掉頁面緩存(開發環境)和html的校驗
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5
12、在shiro的configuration中配置:
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
13、在html中加入xmlns:
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
14、使用標簽。例如:
<span shiro:authenticated="true" >
<span>歡迎您:<span th:text="${userInfo.realName}"></span></span>
</span>
15、標簽說明:
1、用戶沒有身份驗證時顯示相應信息,即游客訪問信息:
<shiro:guest>內容</shiro:guest>
2、用戶已經身份驗證/記住我登錄后顯示相應的信息:
<shiro:user>內容</shiro:user>
3、用戶已經身份驗證通過,即Subject.login登錄成功,不是記住我登錄的:
<shiro:authenticated>內容</shiro:authenticated>
4、顯示用戶身份信息,通常為登錄帳號信息,默認調用Subject.getPrincipal()獲取,即Primary Principal:
<shiro:principal/>
5、用戶已經身份驗證通過,即沒有調用Subject.login進行登錄,包括記住我自動登錄的也屬於未進行身份驗證,與guest標簽的區別是,該標簽包含已記住用戶。:
<shiro:notAuthenticated>內容</shiro:notAuthenticated>
6、<shiro:principal type="java.lang.String"/>
相當於Subject.getPrincipals().oneByType(String.class)。
7、<shiro:principal property="username"/>
相當於((User)Subject.getPrincipals()).getUsername()。
8、如果當前Subject有角色將顯示body體內容:
<shiro:hasRole name="角色名">內容</shiro:hasRole>
9、<shiro:hasAnyRoles name="角色名1,角色名2…">內容</shiro:hasAnyRoles>
如果當前Subject有任意一個角色(或的關系)將顯示body體內容。
10、<shiro:lacksRole name="角色名">內容</shiro:lacksRole>
如果當前Subject沒有角色將顯示body體內容。
11、<shiro:hasPermission name="權限名">內容</shiro:hasPermission>
如果當前Subject有權限將顯示body體內容。
12、<shiro:lacksPermission name="權限名">內容</shiro:lacksPermission>
如果當前Subject沒有權限將顯示body體內容。
補充:
1).需要在templates下創建login.html、index.html、unauthorized.html頁面。
2).自定義Filter,ShiroSessionListener和自定義密碼校驗以及ShiroConfig中的配置項,按需添加即可。