shiro整合springmvc

說明

  代碼及部分相關資料根據慕課網Mark老師的視頻進行整理html

  其餘資料:前端

流程

配置

1) 配置web.xml整合shiro
把shiro整合到springMVC實質上是在web.xml配置過濾器(filter),配置DelegatingFilterProxy,讓其代理shiro的過濾器,對須要認證或者受權的請求路徑進行過濾。java

<!--  DelegatingFilterProxy能夠代理Spring管理的bean中的Filter,shiro的filter就是由其代理;
"filter-name"要與spring配置文件中ShiroFilterFactoryBean的id一致;
這裏至關於把shiro和springmvc整合到一塊兒-->
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

2) 配置spring.xml添加shiro組件
與上一節的程序同樣,須要添加SecutiryManager、Realm兩個核心組件。mysql

  • (非必需)建立HashedCredentialsMatcher。用於加密,也可不加密,根據本身需求進行配置,建議加密。
<!--  1.配置用於密碼解密的HashedCredentialMatcher  -->
<bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    <property name="hashIterations" value="3"/>
    <property name="hashAlgorithmName" value="MD5"/>
</bean>
  • 建立realm。此處示例使用自定義的可加密MyEncryptedRealm,引用HashedCredentialMatcher
<!--  2.配置Realm,使用自定義的MyEncryptedRealm,引用HashedCredentialMatcher  -->
<bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedRealm">
    <property name="credentialsMatcher" ref="matcher"/>
</bean>
  • 建立SecurityManager。示例使用DefaultWebSecurityManager,引用上面的realm。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="realm"/>
</bean>
  • 建立ShiroFilterFactoryBean。該Bean會根據配置,生成一個被DelegatingFilterProxy代理的,類型爲SpringShiroFilter的過濾器,這個過濾器包含FilterChain,用於對請求進行實際上的更詳細的過濾。該Bean的id必須與web.xml中配置的DelegatingFilterProxy的「filter-name」一致。
<!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
該Bean會建立一個shiro的內部類SpringShiroFilter的對象,並交由DelegatingFilterProxy代理-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="login.html"/>
    <property name="unauthorizedUrl" value="403.html"/>
    <!--  ShiroFilterFactoryBean會根據如下配置建立shiro的過濾器鏈  -->
    <property name="filterChainDefinitions">
        <value>
            /login.html = anon
            /subLogin = anon
            /register = anon
            /addPermissions = anon
            /* = authc
        </value>
    </property>
</bean>

filterChain從上到下匹配,當匹配到合適的規則時進行處理,無論後面的規則如何,因此必定要注意順序。 value值的第一個'/'表明的路徑是相對於HttpServletRequest.getContextPath()的值。
anon:它對應的過濾器裏面是空的,什麼都沒作;
authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
shiro包含11個過濾器,具體信息可查看shiro官網
git

實戰1

maven依賴

<!-- springmvc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>
    <!-- shiro相關 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.4.0</version>
    </dependency>
    <!-- 日誌相關 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.26</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>

工程結構

配置文件

web.xml:github

<!DOCTYPE web-app PUBLIC
       "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
       "http://java.sun.com/dtd/web-app_2_3.dtd" >

   <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        version="3.0"
        metadata-complete="true">

   <!--  聲明應用範圍(整個WEB項目)內的上下文初始化參數。  -->
   <context-param>
       <param-name>contextConfigLocation</param-name>
       <!--掃描全部spring配置文件,不用在配置文件裏import-->
       <param-value>classpath*:spring/spring*</param-value>
   </context-param>

   <!--  配置監聽器,用於springIOC -->
   <listener>
       <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
   <!--<listener>
       <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
   </listener>-->

   <!--  DelegatingFilterProxy能夠代理Spring管理的bean中的Filter,shiro的filter就是由其代理;
   "filter-name"要與spring配置文件中ShiroFilterFactoryBean的id一致;
   這裏至關於把shiro和springmvc整合到一塊兒-->
   <filter>
       <filter-name>shiroFilter</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   </filter>
   <filter-mapping>
       <filter-name>shiroFilter</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

   <!--  將請求路由到相應的handler  -->
   <servlet>
       <servlet-name>spring-mvc</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <init-param>
           <param-name>contextConfigLocation</param-name>
           <param-value>classpath*:spring/spring-mvc.xml</param-value>
       </init-param>
       <load-on-startup>1</load-on-startup>
   </servlet>
   <servlet-mapping>
       <servlet-name>spring-mvc</servlet-name>
       <url-pattern>/</url-pattern>
   </servlet-mapping>
   </web-app>

spring.xml:web

<?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">

    <!--  1.配置用於密碼解密的HashedCredentialMatcher  -->
    <bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashIterations" value="3"/>
        <property name="hashAlgorithmName" value="MD5"/>
    </bean>

    <!--  2.配置Realm,使用自定義的MyEncryptedRealm,引用HashedCredentialMatcher  -->
    <bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedRealm">
        <property name="credentialsMatcher" ref="matcher"/>
    </bean>

    <!--  3.配置SecurityManager,引用Realm  -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="realm"/>
    </bean>

    <!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
    該Bean會建立一個shiro的內部類SpringShiroFilter的對象,並交由DelegatingFilterProxy代理-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="login.html"/>
        <property name="unauthorizedUrl" value="403.html"/>
        <!--  ShiroFilterFactoryBean會根據如下配置建立shiro的過濾器鏈  -->
        <property name="filterChainDefinitions">
            <value>
                /login.html = anon
                /subLogin = anon
                /register = anon
                /addPermissions = anon
                /* = authc
            </value>
        </property>
    </bean>
    </beans>

springmvc.xml:redis

<?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--  掃描shiro包下全部組件(包括@Controller、@Component等)  -->
    <context:component-scan base-package="com.lifeofcoding.shiro"></context:component-scan>

    <!-- 1.開啓註解;
         2.註冊HandlerMapping和HandlerAdapter的實現類。
         配置該參數,spring能夠經過context:component-scan/標籤的配置,自動將掃描到的@Component,@Controller,@Service,@Repository等註解標記的組件註冊到工廠中,來處理請求。
         該參數還支持如下功能:
         a:默認提供的功能:數據綁定,數字和日期的format@NumberFormat,@DateTimeFormat
         b:xml,json的默認讀寫支持-->
    <mvc:annotation-driven/>

    <!-- 處理靜態資源 -->
    <mvc:resources mapping="/*" location="/"/>
    </beans>

log4j.properties:算法

# Global logging configuration #\u5728\u5f00\u53d1\u73af\u5883\u4e0b\u65e5\u5fd7\u7ea7\u522b\u8981\u8bbe\u7f6e\u6210DEBUG\uff0c\u751f\u4ea7\u73af\u5883\u8bbe\u7f6e\u6210info\u6216error
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

後端代碼

com.lifeofcoding.shiro.pojo.User.java
package com.lifeofcoding.shiro.pojo;

import java.util.Set;

public class User {
    
    private String username;
    private String password;
    private Set
   
   
   

  
   
  roles; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Set 
 
   
     getRoles() { return roles; } public void setRoles(Set 
    
      roles) { this.roles = roles; } } 
     
    

  
com.lifeofcoding.shiro.realm.MyEncryptedRealm.java
package com.lifeofcoding.shiro.realm;

import com.lifeofcoding.shiro.pojo.User;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class MyEncryptedRealm extends AuthorizingRealm {

    /** 加密次數 */
    private int iterations;
    /** 算法名 */
    private String algorithmName;

    /** 存儲用戶名和密碼 */
    private final Map
   
   
   

  
   
  userMap; /** 存儲用戶及其對應的角色 */ private final Map 
 
   
     > roleMap; /** 存儲全部角色以及角色對應的權限 */ private final Map 
     
     
       > permissionMap; /** 存儲用戶鹽值 */ private Map 
      
        saltMap; { //設置Realm名,可用於獲取該realm super.setName("MyRealm"); } public MyEncryptedRealm(){ this.iterations = 0; this.algorithmName = "MD5"; this.userMap = new ConcurrentHashMap<>(16); this.roleMap = new ConcurrentHashMap<>(16); this.permissionMap = new ConcurrentHashMap<>(16); this.saltMap = new ConcurrentHashMap<>(16); } /** * 身份認證必須實現的方法 * @param authenticationToken token to do authenticate * @return org.apache.shiro.authc.AuthenticationInfo */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //1.獲取主體中的用戶名 String userName = (String) authenticationToken.getPrincipal(); //2.經過用戶名獲取密碼,getPasswordByName自定義實現 String password = getPasswordByUserName(userName); if(null == password){ return null; } //3.構建authenticationInfo認證信息 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm"); //添加鹽值 String salt = getSaltByUsername(userName); authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt)); return authenticationInfo; } /** * 用於受權,必須實現 * @param principalCollection principals * @return org.apache.shiro.authz.AuthorizationInfo */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //1.獲取用戶名。principal爲Object類型,是用戶惟一憑證,能夠是用戶名,用戶郵箱,數據庫主鍵等,能惟一肯定一個用戶的信息。 String userName = (String) principalCollection.getPrimaryPrincipal(); //2.獲取角色信息,getRoleByUserName自定義 Set 
       
         roles = getRolesByUserName(userName); //3.獲取權限信息,getPermissionsByRole方法一樣自定義,也能夠經過用戶名查找權限信息 Set 
        
          permissions = getPermissionsByUserName(userName); //4.構建認證信息並返回。 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setStringPermissions(permissions); simpleAuthorizationInfo.setRoles(roles); return simpleAuthorizationInfo; } /** * 往realm添加帳號信息 * @param user user */ public void addAccount(User user) throws UserExistException { String userName = user.getUsername(); String password = user.getPassword(); Set 
         
           roles = user.getRoles(); if(null != userMap.get(userName)){ throw new UserExistException("user \""+ userName +"\" already exist"); } //若是設置的加密次數大於0,則對密碼進行加密 if(iterations > 0){ //此處用隨機數做爲鹽值,可改成UUID或其它 String salt = String.valueOf(Math.random()*10); saltMap.put(userName,salt); password = doHash(password, salt); } userMap.put(userName, password); //若是roles不爲空,存入roleMap if (!CollectionUtils.isEmpty(roles)){ roleMap.put(userName, roles); } } /** * 自定義部分,經過用戶名獲取權限信息 * @param userName username * @return java.util.Set 
          
            */ private Set 
           
             getPermissionsByUserName(String userName) { //1.先經過用戶名獲取角色信息 Set 
            
              roles = getRolesByUserName(userName); if (CollectionUtils.isEmpty(roles)){ return null; } //2.經過角色信息獲取對應的權限 Set 
             
               permissions = new HashSet<>(); roles.forEach(role -> { if (null != permissionMap.get(role)) { permissions.addAll(permissionMap.get(role)); } }); return permissions; } /** * 自定義部分,經過用戶名獲取密碼,可改成數據庫操做 * @param userName username * @return java.lang.String */ private String getPasswordByUserName(String userName){ return userMap.get(userName); } /** * 自定義部分,經過用戶名獲取角色信息,可改成數據庫操做 * @param userName username * @return java.util.Set 
              
                */ private Set 
               
                 getRolesByUserName(String userName){ return roleMap.get(userName); } /** * 自定義部分,經過用戶名獲取角色信息,可改成數據庫操做 * @param userName username * @return java.util.Set 
                
                  */ private String getSaltByUsername(String userName) { return saltMap.get(userName); } /** * 往realm刪除帳號信息 * @param userName username */ public void deleteAccount(String userName){ userMap.remove(userName); roleMap.remove(userName); } /** * 添加角色權限,變參不傳值會接收到長度爲0的數組。 * @param roleName name of the role * @param permissions permissions which this role preserve */ public void addPermissions(String roleName,Set 
                 
                   permissions){ permissionMap.put(roleName, permissions); } /** * 設置加密次數 * @param iterations iterations to doHash */ public void setHashIterations(int iterations){ this.iterations = iterations; } /** * 設置算法名 * @param algorithmName name of the algorithm to use */ public void setAlgorithmName(String algorithmName){ this.algorithmName = algorithmName; } /** * 計算哈希值 * @param str str to doHash * @param salt user's salt */ private String doHash(String str,String salt){ salt = null==salt ? "" : salt; return new SimpleHash(this.algorithmName,str,salt,this.iterations).toString(); } /** * 註冊時,用戶已存在的異常 */ public class UserExistException extends Exception{ private UserExistException(String message) {super(message);} } } 
                  
                 
                
               
              
             
            
           
          
         
        
       
      
     
    

  
com.lifeofcoding.shiro.controller.UserController.java
package com.lifeofcoding.shiro.controller;

import com.lifeofcoding.shiro.pojo.User;
import com.lifeofcoding.shiro.realm.MyEncryptedRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.ResponseBody;

@Controller
public class UserController {

    @Autowired
    private MyEncryptedRealm realm;

    /**
     * 用戶登陸
     * @param user 用戶信息,包括用戶名(username)和密碼(password)
     * api示例: POST /subLogin?username=java&password=123
     * */
    @ResponseBody
    @RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
    public String subLogin(User user){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
        try {
            subject.login(token);
        }catch (Exception e){
            return e.getMessage();
        }
        return "\""+subject.getPrincipal().toString()+"\""+"登錄成功";
    }

    /**
     * 用戶註冊
     * @param user 用戶信息,包括:用戶名(username)、密碼(password)、角色(roles)(可選)
     * @return 返回註冊信息
     * api示例: POST /register?username=java&password=123&roles=admin&roles=user  可經過指定多個roles傳入roles數組
     * */
    @ResponseBody
    @RequestMapping(value = "/register",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
    public String register(User user){
        //配置realm設置加密方式
        realm.setAlgorithmName("MD5");
        //加密次數
        realm.setHashIterations(3);
        //添加帳號
        try {
            realm.addAccount(user);
        }catch (Exception e){
            return e.getMessage();
        }
        return "Add account \"" + user.getUsername() + "\" succeeded";
    }

    /**
     * 測試已登陸的用戶是否擁有某角色
     * @param role 角色名
     * @return 返回信息
     * api示例: GET /testRole?role=admin
     * */
    @ResponseBody
    @RequestMapping(value = "testRole",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String testRole(String role){
        if (null == role){
            return "no input";
        }
        Subject subject = SecurityUtils.getSubject();
        if (subject.hasRole(role)){
            return "user \"" + subject.getPrincipal()+"\" has role \"" + role +"\"";
        }
        return  "user \"" + subject.getPrincipal()+"\" do not have role \"" + role + "\"";
    }

    /**
     * 測試已登陸的用戶是否擁有某權限
     * @param permission 權限
     * @return 返回信息
     * api示例: GET /testPermission?permission=user:delete
     * */
    @ResponseBody
    @RequestMapping(value = "testPermission",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String testPermission(String permission){
        if (null == permission){
            return "no input";
        }
        Subject subject = SecurityUtils.getSubject();
        if (subject.isPermitted(permission)){
            return "user \"" + subject.getPrincipal()+"\" has permission \"" + permission +"\"";
        }
        return  "user \"" + subject.getPrincipal()+"\" do not have permission \"" + permission + "\"";
    }

    /**
     * 添加權限
     * api示例: GET /addPermissions?role=admin&permissions=user:delete&permissions=user:modify
     * */
    @ResponseBody
    @RequestMapping(value = "addPermissions",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String addPermissions(String role, String...permissions){
        if (role==null || CollectionUtils.isEmpty(CollectionUtils.asSet(permissions))){
            return "rolename or permissions can not be empty";
        }
        realm.addPermissions(role, CollectionUtils.asSet(permissions));
        return null;
    }
}

前端代碼

login.html:spring

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="subLogin" method="post">
    用戶名: <input type="text" name="username"/>\</br>
    密碼: <input type="password" name="password"/>\</br>
    <input type="submit" value="登陸">
</form>

</body>
</html>

實戰2——自定義jdbcRealm

代碼與實戰1基本一致,僅僅是修改Realm,改成從數據庫中獲取信息,再修改相關配置。

maven依賴

<dependencies>
    <!--  shiro  -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
    <!--  spring  -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>
    <!--  日誌相關  -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.26</version>
    </dependency>
    <!--  數據庫相關  -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.15</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.6</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>4.3.5.RELEASE</version>
    </dependency>
    <!-- AOP相關 aspectjweaver(用於切入點表達式)包含aspectjrt(用於aop相關注解),所以只引入前者-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.10</version>
    </dependency>
    </dependencies>

項目結構

配置文件

spring.xml:

<?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">

    <!--  1.配置用於密碼解密的CredentialMatcher  -->
    <bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashIterations" value="3"/>
        <property name="hashAlgorithmName" value="MD5"/>
    </bean>

    <!--  2.配置Realm,使用自定義的MyEncryptedJdbcRealm,引用Matcher  -->
    <bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedJdbcRealm">
        <property name="credentialsMatcher" ref="matcher"/>
    </bean>

    <!--  3.配置SecurityManager,引用Realm  -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="realm"/>
    </bean>

    <!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
    該Bean會建立一個shiro的內部類SpringShiroFilter的對象,並交由DelegatingFilterProxy代理-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="login.html"/>
        <property name="unauthorizedUrl" value="403.html"/>
        <!--  ShiroFilterFactoryBean會根據如下配置建立shiro的過濾器鏈  -->
        <property name="filterChainDefinitions">
            <value>
                /login.html = anon
                /subLogin = anon
                /register = anon
                /addPermissions = anon
                /testPermission = anon
                /testRole = anon
                /* = authc
            </value>
        </property>
    </bean>
    </beans>

spring-dao.xml:

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation=
               "http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置數據源dataSource -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="root"/>
        <property name="password" value="0113"/>
        <property name="url" value="jdbc:mysql://localhost:3306/shiro"/>
    </bean>

    <!-- 配置JdbcTemplate,引用dataSource -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事務管理器transactionManager,引用dataSource -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- spring-tx模塊以AOP方式管理spring中的事務 -->
    <!-- 配置AOP全局事務,設置通知(Advice)的屬性 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置AOP全局事務,設置切面(Aspect),引入txAdvice -->
    <aop:config proxy-target-class="true">
        <!--  對realm包下,以"add"和"delete"開頭的方法開啓事務 -->
        <aop:advisor advice-ref="txAdvice"
                     pointcut="execution(* com.lifeofcoding.shiro.realm..*.add*(..))
                           or execution(* com.lifeofcoding.shiro.realm..*.delete*(..))"/>
    </aop:config>
    </beans>

spring-mvc.xml:

<?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--  掃描shiro包下全部組件(包括@Controller、@Component等)  -->
    <context:component-scan base-package="com.lifeofcoding.shiro"/>

    <!-- 1.開啓註解;
         2.註冊HandlerMapping和HandlerAdapter的實現類。
         配置該參數,spring能夠經過context:component-scan/標籤的配置,自動將掃描到的@Component,@Controller,@Service,@Repository等註解標記的組件註冊到工廠中,來處理請求。
         該參數還支持如下功能:
         a:默認提供的功能:數據綁定,數字和日期的format@NumberFormat,@DateTimeFormat
         b:xml,json的默認讀寫支持-->
    <mvc:annotation-driven/>

    <!-- 處理靜態資源 -->
    <mvc:resources mapping="/*" location="/"/>
    </beans>

log4j.properties:

\# Global logging configuration \#\u5728\u5f00\u53d1\u73af\u5883\u4e0b\u65e5\u5fd7\u7ea7\u522b\u8981\u8bbe\u7f6e\u6210DEBUG\uff0c\u751f\u4ea7\u73af\u5883\u8bbe\u7f6e\u6210info\u6216error
log4j.rootLogger=DEBUG, stdout
\# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

後臺代碼

UserDaoImpl.java
package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.UserDao;
import com.lifeofcoding.shiro.pojo.User;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component
public class UserDaoImpl implements UserDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public String getPasswordByUserName(String userName) {
        String queryPasswordSql = "SELECT password FROM shiro_web_users WHERE username = ?";
        List
   
   
   

  
   
  passwords = jdbcTemplate.query(queryPasswordSql, new String[]{userName}, new RowMapper 
 
   
     () { @Override public String mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getString("password"); } }); if(CollectionUtils.isEmpty(passwords)){ return null; } return passwords.get(0); } @Override public Set 
    
      getRolesByUserName(String userName) { String queryRoleSql = "SELECT role FROM shiro_web_user_roles WHERE username = ?"; List 
     
       roles = jdbcTemplate.query(queryRoleSql, new String[]{userName}, new RowMapper 
      
        () { @Override public String mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getString("role"); } }); if (CollectionUtils.isEmpty(roles)){ return null; } return new HashSet<>(roles); } @Override public String getSaltByUserName(String userName) { String querySaltSql = "SELECT salt FROM shiro_web_users WHERE username = ?"; List 
       
         salts = jdbcTemplate.query(querySaltSql,new String[]{userName},new RowMapper 
        
          () { @Override public String mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getString("salt"); } }); if (CollectionUtils.isEmpty(salts)){ return null; } return salts.get(0); } @Override public void addUser(User user) throws Exception{ if (user == null){ return; } String addUserSql = "INSERT INTO shiro_web_users (username,password,salt) VALUES (?,?,?)"; jdbcTemplate.update(addUserSql,new Object[]{user.getUsername(),user.getPassword(),user.getSalt()}); } @Override public void deleteUser(String userName) { String deleteUserSql = "DELETE FROM shiro_web_users WHERE username = ?"; jdbcTemplate.update(deleteUserSql,userName); } } 
         
        
       
      
     
    

  
PermissionDaoImpl.java
package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.PermissionDao;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component
public class PermissionDaoImpl implements PermissionDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public void addPermissions(String roleName, Set
   
   
   
   
    
    
    
     permissions) {
        String addPermissionSql = "INSERT IGNORE INTO shiro_web_roles_permissions (role,permission) VALUES (?,?)";
        //去掉空數據
        permissions.remove("");
        //後面StatementSetter須要用index遍歷集合,因此轉爲List
        ArrayList
    
    
    
    
      tempPermissions = new ArrayList<>(permissions); //批量添加數據 jdbcTemplate.batchUpdate(addPermissionSql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, roleName); ps.setString(2, tempPermissions.get(i)); } @Override public int getBatchSize() { return tempPermissions.size(); } }); } @Override public Set 
     
       getPermissionsByRole(String role) { String queryPermissionSql = "SELECT permission FROM shiro_web_roles_permissions WHERE role = ?"; List 
      
        permissions = jdbcTemplate.query(queryPermissionSql, new String[]{role}, new RowMapper 
       
         () { @Override public String mapRow(ResultSet resultSet, int i) throws SQLException { return resultSet.getString("permission"); } }); if (CollectionUtils.isEmpty(permissions)){ return null; } return new HashSet<>(permissions); } @Override public void deletePermissionsByRole(String role) { String deletePermissionsByRoleSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ?"; jdbcTemplate.update(deletePermissionsByRoleSql,role); } @Override public void deletePermission(String permission) { String deletePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE permission = ?"; jdbcTemplate.update(deletePermissionSql,permission); } @Override public void deleteRolePermission(String role, String permission) { String deleteRolePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ? AND permission = ?"; jdbcTemplate.update(deleteRolePermissionSql,new Object[]{role,permission}); } } 
        
       
      
    
   
   
   
   
RoleDaoImpl.java
package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.RoleDao;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Set;

@Component
public class RoleDaoImpl implements RoleDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public void addRole(String username, Set
   
   
   
   
    
    
    
     roles) {
        //去掉空數據
        roles.remove("");
        String addRoleSql = "INSERT IGNORE INTO shiro_web_user_roles (username,role) VALUES (?,?)";
        //StatementSetter用index遍歷集合,轉爲List
        ArrayList
    
    
    
    
      tempRoles = new ArrayList<>(roles); //批量添加數據 jdbcTemplate.batchUpdate(addRoleSql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, username); ps.setString(2, tempRoles.get(i)); } @Override public int getBatchSize() { return tempRoles.size(); } }); } @Override public void deleteRolesByUsername(String userName) { String deleteRoleByUsernameSql = "DELETE FROM shiro_web_user_roles WHERE username = ?"; jdbcTemplate.update(deleteRoleByUsernameSql,userName); } @Override public void deleteRole(String role) { String deleteRoleSql = "DELETE FROM shiro_web_user_roles WHERE role = ?"; jdbcTemplate.update(deleteRoleSql,role); } @Override public void deleteUserRole(String userName, String role) { String deleteUserRoleSql = "DELETE FROM shiro_web_user_roles WHERE username = ? AND role = ?"; jdbcTemplate.update(deleteUserRoleSql,new Object[]{userName,role}); } } 
    
   
   
   
   
MyEncryptedJdbcRealm.java
package com.lifeofcoding.shiro.realm;

import com.lifeofcoding.shiro.dao.PermissionDao;
import com.lifeofcoding.shiro.dao.RoleDao;
import com.lifeofcoding.shiro.dao.UserDao;
import com.lifeofcoding.shiro.pojo.User;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;

public class MyEncryptedJdbcRealm extends AuthorizingRealm {
    @Resource
    private UserDao userDao;
    @Resource
    private PermissionDao permissionDao;
    @Resource
    private RoleDao roleDao;

    /**加密次數*/
    private int iterations;
    /**加密算法名*/
    private String algorithmName;

    /*---------------------------------實現自定義Realm須要重寫的兩個方法------------------------------------*/

    /**
     * 身份認證必須實現的方法
     * @param authenticationToken token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.獲取主體中的用戶名。principal爲Object類型,是用戶惟一憑證,能夠是用戶名,用戶郵箱,數據庫主鍵等,能惟一肯定一個用戶的信息。
        String userName = (String) authenticationToken.getPrincipal();
        //2.經過用戶名獲取密碼,getPasswordByName自定義實現
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //3.若是密碼不爲空,則構建authenticationInfo認證信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        String salt = getSaltByUserName(userName);
        //4.認證信息添加鹽值
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
        return authenticationInfo;
    }

    /**
     * 用於受權,必須實現
     * @param principalCollection principal的集合
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.獲取用戶名。principal爲Object類型,是用戶惟一憑證,能夠是用戶名,用戶郵箱,數據庫主鍵等,能惟一肯定一個用戶的信息。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.獲取角色信息,getRoleByUserName自定義
        Set
   
   
   
   
    
    
    
     roles = getRolesByUserName(userName);
        //3.獲取權限信息,getPermissionsByRole方法一樣自定義,也能夠經過用戶名查找權限信息
        Set
    
    
    
    
      permissions = getPermissionsByUserName(userName); //4.構建認證信息並返回。 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //添加權限信息 simpleAuthorizationInfo.setStringPermissions(permissions); //添加角色信息 simpleAuthorizationInfo.setRoles(roles); return simpleAuthorizationInfo; } //類加載時初始化 { //設置Realm名,可用於獲取該realm super.setName("MyJdbcRealm"); } /**構造方法,初始化哈希次數及算法名稱*/ MyEncryptedJdbcRealm(){ iterations = 0; algorithmName = "MD5"; } /*--------------------------------------自定義部分--------------------------*/ /** * 自定義部分,經過用戶名獲取權限信息 * @param userName username * @return 該用戶擁有的全部權限 */ public Set 
     
       getPermissionsByUserName(String userName) { //1.先經過用戶名獲取全部角色信息 Set 
      
        roles = userDao.getRolesByUserName(userName); //2.經過角色信息獲取對應的權限 Set 
       
         permissions = new HashSet<>(); roles.forEach(role -> { Set 
        
          tempPermissions = permissionDao.getPermissionsByRole(role); if (null != tempPermissions) { permissions.addAll(tempPermissions); } }); return permissions; } /** * 自定義部分,經過用戶名獲取密碼 * @param userName username * @return java.lang.String */ public String getPasswordByUserName(String userName){ return userDao.getPasswordByUserName(userName); } /** * 自定義部分,經過用戶名獲取鹽 * @param userName username * @return java.lang.String */ public String getSaltByUserName(String userName){ return userDao.getSaltByUserName(userName); } /** * 自定義部分,經過用戶名獲取角色信息 * @param userName username * @return java.util.Set 
         
           */ public Set 
          
            getRolesByUserName(String userName){ return userDao.getRolesByUserName(userName); } /** * 往realm添加帳號信息 * @param user user */ public void addAccount(User user) throws Exception { String salt = ""; String password = user.getPassword(); String userName = user.getUsername(); //用戶信息爲空拋出異常 if (user.getUsername()==null || user.getPassword()==null){ throw new InfoEmptyException("username or password can not be empty"); } //若是用戶已經註冊,拋出異常 if(null != userDao.getPasswordByUserName(userName)){ throw new UserExistException("user \""+ userName +"\" already exist"); } //若是設置的加密次數大於0,則進行加密 if(iterations > 0){ salt = randomSalt(); password = doHash(password, salt); } user.setPassword(password); user.setSalt(salt); userDao.addUser(user); if (CollectionUtils.isEmpty(user.getRoles())){ return; } roleDao.addRole(userName,user.getRoles()); } /** * 添加角色權限 * @param roleName 角色名 * @param permissions 該角色擁有的權限 */ public void addPermissions(String roleName, Set 
           
             permissions) throws Exception{ permissionDao.addPermissions(roleName,permissions); } /** * 用隨機數做爲鹽值,可改成UUID或其餘 * */ public String randomSalt(){ return String.valueOf(Math.random()*10); } /** * 刪除帳號信息 * @param userName 用戶名 */ public void deleteAccount(String userName) throws Exception{ userDao.deleteUser(userName); roleDao.deleteRolesByUsername(userName); } /** * 設置加密次數 * @param iterations 哈希操做的次數 */ public void setHashIterations(int iterations){ this.iterations = iterations; } /** * 設置算法名 * @param algorithmName 哈希算法名 */ public void setAlgorithmName(String algorithmName){ this.algorithmName = algorithmName; } /** * 進行哈希運算 * @param source 原來的字符 * @param salt 鹽值 * @return 運算結果 * */ private String doHash(String source, String salt){ return new SimpleHash(this.algorithmName,source,salt,this.iterations).toString(); } /** * 註冊時,用戶已存在的異常類 */ public class UserExistException extends Exception{ public UserExistException(String message) {super(message);} } /** * 用戶信息爲空的異常 * */ public class InfoEmptyException extends Exception{ public InfoEmptyException(String message) {super(message);} } } 
            
           
          
         
        
       
      
    
   
   
   
   

實戰3——經過註解受權

配置

在springmvc配置文件中添加以下配置,務必在springmvc配置文件中添加,即上面的springmvc.xml文件。

<!-- 開啓AOP -->
<aop:config proxy-target-class="true"/>

<!-- 用於管理shiro的生命週期 -->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- 用於註解方式驗證權限的通知 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

後端代碼

直接在controller上添加註解"@RequiresRoles"或者"@RequiresPermissions",如:

@RequiresPermissions("user:delete")
@RequiresRoles("admin")
@ResponseBody
@RequestMapping(value = "testRole",method = RequestMethod.GET)
public String testRole(){
    return "has role: admin";
}

使用擁有指定角色或者權限的用戶登陸,便可訪問到該"testRole()"方法,不然會拋異常。
也能夠用數組傳多個參數進行受權,如:

@RequiresPermissions({"user:delete","user:login"})
@RequiresRoles({"user","admin"})

噹噹前用戶同時擁有全部指定的角色或者權限時,才能訪問方法。

實戰4——redis實現session管理

實現session管理,主要是給SecurityManager配置SessionManager,而SessionManager,須要配置用於Session增刪查改的SessionDao。SessionDao繼承AbstractSessionDAO抽象類,須要實現的方法有:

  • Serializable doCreate(Session session)
    存儲session
  • Session doReadSession(Serializable sessionId)
    讀取session
  • void update(Session session) throws UnknownSessionException
    更新session
  • void delete(Session session)
    刪除session
  • Collection getActiveSessions()
    獲取活躍的session

maven

添加redis依賴

<!-- redis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.0</version>
</dependency>

後臺代碼

封裝jedis的增刪查改操做:

JedisUtil.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.HashSet;
import java.util.Set;

@Component
public class JedisUtil {
/**jedis鏈接池*/
@Autowired
private JedisPool jedisPool;


}

/**獲取資源*/ private Jedis getResource(){ return jedisPool.getResource(); } /** * set * */ public byte[] set(byte[] key, byte[] value) { Jedis jedis = getResource(); try{ jedis.set(key, value); return value; }finally { jedis.close(); } } /** * 設置過時時間 * */ public void expire(byte[] key, int seconds) { Jedis jedis = getResource(); try { jedis.expire(key,seconds); } finally { jedis.close(); } } /** * 獲取值 * */ public byte[] get(byte[] key) { Jedis jedis = getResource(); try { return jedis.get(key); } finally { jedis.close(); } } /** * 刪除 * */ public void del(byte[] key) { Jedis jedis = getResource(); try { jedis.del(key); } finally { jedis.close(); } } /** * "keys"操做 * */ public Set<byte[]> keys(String pattern) { Jedis jedis = getResource(); try { return jedis.keys((pattern).getBytes()); } finally { jedis.close(); } } /** * 使用scan獲取全部匹配的keys,redis2.8+開始,加入了"scan"操做, * 容許每次只獲取一部分數據,避免數據量大時"keys"形成阻塞 * */ public Set<byte[]> scan(String pattern){ Jedis jedis = getResource(); //初始化遊標 byte[] START_CURSOR = "0".getBytes(); //每次要求返回的數據量 int NUM_PER_SCAN = 50; try{ //設置初始化遊標 byte[] cursor = START_CURSOR; //查詢參數對象 ScanParams params = new ScanParams(); //設置匹配模式 params.match(pattern.getBytes()); //設置理想的每次返回的數據數量(不必定會返回這麼多) params.count(NUM_PER_SCAN); //用一個HashSet來存儲查找到的keys,由於結果可能會重複,因此用set去重 Set<byte[]> keys = new HashSet<>(); while(true) { /*redis的scan與單循環鏈表類似,每次scan操做,返回部分數據result以及下次scan操做須要的遊標cursor*/ ScanResult result = jedis.scan(cursor,params); //獲取下次scan的遊標,byte[]類型,若是是String類型,返回結果也會是String類型,須要注意。 cursor = result.getCursorAsBytes(); keys.addAll(result.getResult()); //若是已經遍歷完全部數據,則退出 if(result.isCompleteIteration()) {break;} } return keys; }finally { jedis.close(); } }

AbstractSessionDAO的子類:

RedisSessionDao.java

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SerializationUtils;
import com.lifeofcoding.utils.JedisUtil;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class RedisSessionDao extends AbstractSessionDAO {


}

/**封裝的redis工具類*/ @Resource private JedisUtil jedisUtil; /**在redis中存儲的session的前綴*/ private final String SHIRO_SESSION_PREFIX="shiro-session:"; /** * 把傳入的key(sessionId)轉化爲在redis中存儲的統一格式的key * */ private byte[] getKey(String key){ return (SHIRO_SESSION_PREFIX+key).getBytes(); } /** * 保存session到redis中 * */ private void saveSession(Session session){ if (null != session && null != session.getId()) { //獲取session的id並將其傳化爲指定格式 byte[] key = getKey(session.getId().toString()); //對session進行序列化 byte[] value = SerializationUtils.serialize(session); jedisUtil.set(key, value); jedisUtil.expire(key, 600); } } /** * 把session保存到redis * */ @Override protected Serializable doCreate(Session session) { //建立sessionId Serializable sessionId = generateSessionId(session); //給session綁定sessionId assignSessionId(session,sessionId); //保存session到redis中 saveSession(session); return sessionId; } /** * 讀取session * */ @Override protected Session doReadSession(Serializable sessionId) { if (null == sessionId) { return null; } //把sessionId轉化爲redis中的key的格式 byte[] key = getKey(sessionId.toString()); byte[] value = jedisUtil.get(key); //返回反序列化後的session return (Session) SerializationUtils.deserialize(value); } /** * 更新session * */ @Override public void update(Session session) throws UnknownSessionException { saveSession(session); } /** * 刪除session * */ @Override public void delete(Session session) { if (null == session && null == session.getId()){ return; } byte[] key = getKey(session.getId().toString()); jedisUtil.del(key); } /** * 獲取活躍的session * */ @Override public Collection<Session> getActiveSessions() { //獲取redis中存儲session的全部key //Set<byte[]> keys = jedisUtil.keys(SHIRO_SESSION_PREFIX+"*"); //能夠本身改寫、優化scan,用"scan"操做替代"keys",避免數據量大時阻塞。 Set<byte[]> keys = jedisUtil.scan(SHIRO_SESSION_PREFIX+"*"); Set<Session> sessions = new HashSet<Session>(); if (CollectionUtils.isEmpty(keys)){ return sessions; } for (byte[] key : keys){ Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key)); sessions.add(session); } return sessions; }

配置文件

redis的配置文件:

spring-redis.xml



<?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="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"/> <!-- 建立鏈接池 --> <bean id="jedisPool" class="redis.clients.jedis.JedisPool"> <constructor-arg name="poolConfig" ref="jedisPoolConfig"/> <constructor-arg name="host" value="127.0.0.1"/> <constructor-arg name="port" value="6379"/> <!--<constructor-arg name="timeout" value="60000"/>--> <!--<constructor-arg name="password" value="123"/>--> </bean>

實現了AbstractSessionDao抽象類後,在spring配置文件中配置該實現類,而後配置SessionManager。

<!--  配置自定義的sessionDao  -->
<bean id="redisSessionDao" class="com.lifeofcoding.session.RedisSessionDao"/>

<!--  經過sessionDao配置sessionManager  -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
    <property name="sessionDAO" ref="redisSessionDao"/>
</bean>

而後在securityManager中配置sessionManager

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="realm"/>
    <!--  配置sessionManager  -->
    <property name="sessionManager" ref="sessionManager"/>
</bean>

SessionManager優化


  使用DefaultSessionManager管理session時,session經過retrieveSession(SessionKey sessionKey)方法獲取,該方法又調用retrieveSessionFromDataSource(sessionId),利用SessionDao從數據源中獲取session,此處sessionDao就是以前的本身實現的RedisSessionDao,而「數據源」,就是redis。
  經過debug能夠發現有時候在處理一次請求時,retrieveSession方法調用了不少次,這樣就意味着訪問了不少次redis,這給redis帶來了沒必要要的壓力。此時,能夠重寫該方法,把session存儲到request中,須要獲取session時,直接從request中獲取,避免redis服務器沒必要要的開銷。
  自定義SessionManager,須要繼承 DefaultSessionManager的子類DefaultWebSessionManager,而不是直接繼承DefaultSessionManager,不然獲取到的sessionId和request爲null;
代碼以下:

CustomSessionManager.java

package com.lifeofcoding.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;

public class CustomSessionManager extends DefaultWebSessionManager {


}

@Override protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException{ //經過SessionKey獲取SessionId Serializable sessionId = getSessionId(sessionKey); ServletRequest request = null; //經過SessionKey獲取ServletRequest if (sessionKey instanceof WebSessionKey) { request = ((WebSessionKey) sessionKey).getServletRequest(); } //嘗試從request中根據sessionId獲取session if (null!=request && null!=sessionId){ Session session = (Session) request.getAttribute(sessionId.toString()); if (null!=session) { return session; } } /*若是request中沒有session,則使用父類獲取session,並保存到request中, 父類DefaultWebSession是經過SessionDao獲取session,在這裏是從redis獲取*/ Session session = super.retrieveSession(sessionKey); if (null != request && null != sessionId){ request.setAttribute(sessionId.toString(),session); } return session; }

自定義SessionManager後,修改配置文件,把DefaultSessionManager改成本身的SessionManager。

<!--  使用自定義的sessionManager,減小對redis的壓力  -->
<bean id="sessionManager" class="com.lifeofcoding.session.CustomSessionManager">
    <property name="sessionDAO" ref="redisSessionDao"/>
</bean>

實戰5——使用redis實現緩存管理

  在程序中,對用戶權限數據的訪問量是比較大的,若是每次受權,都去數據庫中取數據,這是十分不理想的,能夠用redis來充當緩存,緩存用戶的受權數據,減輕數據庫壓力。

後端代碼

1.繼承Cache類,編寫RedisCache,用於對redis中的緩存數據進行增刪查改。Cache類實質上至關於DAO,僅僅是對緩存進行增刪查改。

RedisCache.java

package com.lifeofcoding.shiro.cache;

import com.lifeofcoding.shiro.utils.JedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Set;

@Component
public class RedisCache<K,V> implements Cache<K,V> {


}

@Resource private JedisUtil jedisUtil; /** * cache的前綴 * */ private final String CACHE_PREFIX = "shiro-cache:"; private byte[] getKey(K k){ if (k instanceof String){ return (CACHE_PREFIX + k).getBytes(); } return SerializationUtils.serialize(k); } @Override public V get(K k) throws CacheException { System.out.println("read cache from redis for user: "+k.toString()); byte[] value = jedisUtil.get(getKey(k)); if (null != value){ return (V) SerializationUtils.deserialize(value); } return null; } @Override public V put(K k, V v) throws CacheException { byte[] key = getKey(k); byte[] value = SerializationUtils.serialize(v); jedisUtil.set(key,value); jedisUtil.expire(key,600); return v; } @Override public V remove(K k) throws CacheException { byte[] key = getKey(k); byte[] value = jedisUtil.get(key); jedisUtil.del(key); if (null != value){ return (V) SerializationUtils.deserialize(value); } return null; } @Override public void clear() throws CacheException { } @Override public int size() { return 0; } @Override public Set<K> keys() { return null; } @Override public Collection<V> values() { return null; }

2.繼承CacheManager,編寫RedisCacheManager,用來返回cache。CacheManager只有一個方法「getCache(String var1)」,經過傳入cache的名字,返回對應的cache,僅此而已。

RedisCacheManager.java

package com.lifeofcoding.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import javax.annotation.Resource;

public class RedisCacheManager implements CacheManager {
@Resource
private RedisCache redisCache;


}

/** * 該方法用來給shiro獲取cache對象。 * 參數s爲cache的名稱,此處只有一個cache,即RedisCache,直接返回單例的RedisCache實例便可。 * */ @Override public <K, V> Cache<K, V> getCache(String s) throws CacheException { return redisCache; }
<

配置文件

給SecurityManager配置CacheManager

<!--  5.配置CacheManager  -->
<bean id="cacheManager" class="com.lifeofcoding.shiro.cache.RedisCacheManager"/>

<!--  6.配置SecurityManager,引用Realm、SessionManager、CacheManager  -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="realm"/>
    <property name="sessionManager" ref="sessionManager"/>
    <property name="cacheManager" ref="cacheManager"/>
</bean>

拓展

把受權數據放redis,每次須要受權數據時就訪問redis,這對redis的資源也形成必定浪費,能夠在RedisCache中用Map等集合類,構造二級緩存,每次須要數據,直接從二級緩存中獲取,若是沒有數據,再從redis中取。

實戰6——RememberMe

不少狀況下,網站須要提供「記住我」的功能,可使用shiro的CookieRememberMeManager實現。在配置方面只需在spring配置文件中添加配置便可。

<!--  6.設置cookie名稱和時間,cookie保存加密的用戶信息,可在瀏覽器開發者工具查看   -->
<bean id="simpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <property name="name" value="rememberMeCookie"/>
    <property name="maxAge" value="600"/>
</bean>

<!--  7.設置RememberMeManager,引用cookie  -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <property name="cookie" ref="simpleCookie"/>
</bean>

<!--  8.配置SecurityManager,引用Realm、SessionManager、cacheManager和RememberMeManager  -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="realm"/>
    <property name="sessionManager" ref="sessionManager"/>
    <property name="cacheManager" ref="cacheManager"/>
    <property name="rememberMeManager" ref="rememberMeManager"/>
</bean>

固然,也要修改User類,添加rememberMe字段,讓用戶自行決定是否啓用該功能,同時修改UserController實現該功能。

private boolean rememberMe;

public boolean getRememberMe() {
    return rememberMe;
}

public void setRememberMe(boolean rememberMe) {
    this.rememberMe = rememberMe;
}
@ResponseBody
@RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
public String subLogin(User user){
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
    try {
        //設置自動登陸
        token.setRememberMe(user.getRememberMe());
        subject.login(token);
    }catch (Exception e){
        return e.getMessage();
    }
    return "\""+subject.getPrincipal().toString()+"\""+"登錄成功";
}

文件傳送門

github地址

相關文章
相關標籤/搜索