Springboot集成Shiro

Shiro提供身份驗證、受權、企業會話管理和加密等功能。css

一、添加依賴:html

<!-- 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>

二、在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>

三、實現自定義的ShiroRealm.java類: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請求假如沒登陸,那麼這個請求給用戶的感受就是沒有任何反應,而用戶又不知道用戶已經退出了。
     * 在線顯示,在線用戶管理(踢出登陸)。
     * 登陸註冊密碼加密傳輸。
     * 記住個人功能。關閉瀏覽器後仍是登陸狀態。
     */
}

四、若有須要實現自定義的密碼校驗CredentialsMatcher.java:git

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);
    }
    
}

五、根據須要,選擇建立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();
    }
}

六、根據須要建立自定義Filter:github

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;  
    }  
}

六、建立Shiro配置類,可用shiro-conf.xml代替:web

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定義說明 一、一個URL能夠配置多個Filter,使用逗號分隔 二、當設置多個過濾器時,所有驗證經過,才視爲經過
     * 三、部分過濾器可指定參數,如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;
    }
}

七、未受權的設置不生效,須要添加未受權異常處理:ajax

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;
    }
}

八、登陸和退出Controller:redis

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";
    }
}

九、權限測試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";
    }
}

十、項目用的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>

十一、application.properties配置thymeleaf:

#thymeleaf的配置是去掉頁面緩存(開發環境)和html的校驗
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5

十二、在shiro的configuration中配置:

@Bean
public ShiroDialect shiroDialect() {
       return new ShiroDialect();
}

1三、在html中加入xmlns:

<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

1四、使用標籤。例如:

<span shiro:authenticated="true" >
        <span>歡迎您:<span th:text="${userInfo.realName}"></span></span>
</span>

1五、標籤說明:

一、用戶沒有身份驗證時顯示相應信息,即遊客訪問信息:
<shiro:guest>內容</shiro:guest>
二、用戶已經身份驗證/記住我登陸後顯示相應的信息:
<shiro:user>內容</shiro:user>
三、用戶已經身份驗證經過,即Subject.login登陸成功,不是記住我登陸的:
<shiro:authenticated>內容</shiro:authenticated>
四、顯示用戶身份信息,一般爲登陸賬號信息,默認調用Subject.getPrincipal()獲取,即Primary Principal:
<shiro:principal/>
五、用戶已經身份驗證經過,即沒有調用Subject.login進行登陸,包括記住我自動登陸的也屬於未進行身份驗證,與guest標籤的區別是,該標籤包含已記住用戶。:
<shiro:notAuthenticated>內容</shiro:notAuthenticated> 
六、<shiro:principal type="java.lang.String"/>
至關於Subject.getPrincipals().oneByType(String.class)。 
七、<shiro:principal property="username"/> 
至關於((User)Subject.getPrincipals()).getUsername()。  
八、若是當前Subject有角色將顯示body體內容:
<shiro:hasRole name="角色名">內容</shiro:hasRole>
九、<shiro:hasAnyRoles name="角色名1,角色名2…">內容</shiro:hasAnyRoles>
若是當前Subject有任意一個角色(或的關係)將顯示body體內容。
十、<shiro:lacksRole name="角色名">內容</shiro:lacksRole>
若是當前Subject沒有角色將顯示body體內容。
十一、<shiro:hasPermission name="權限名">內容</shiro:hasPermission>
若是當前Subject有權限將顯示body體內容。 
十二、<shiro:lacksPermission name="權限名">內容</shiro:lacksPermission>
若是當前Subject沒有權限將顯示body體內容。

補充:

1).須要在templates下建立login.html、index.html、unauthorized.html頁面。
2).自定義Filter,ShiroSessionListener和自定義密碼校驗以及ShiroConfig中的配置項,按需添加便可。
相關文章
相關標籤/搜索