1. 程式人生 > >Shiro學習筆記(2)——身份驗證之Realm

Shiro學習筆記(2)——身份驗證之Realm

環境準備

  • 建立java工程

  • 需要的jar包

這裡寫圖片描述

  • 大家也可以使用maven,參考官網

什麼是Realm

  • 在我所看的學習資料中,關於Realm的定義,寫了整整一長串,但是對於初學者來說,看定義實在是太頭疼了。

  • 對於什麼是Realm,我使用過之後,個人總結一下:shiro要進行身份驗證,就要從realm中獲取相應的身份資訊來進行驗證,簡單來說,我們可以自行定義realm,在realm中,從資料庫獲取身份資訊,然後和 使用者輸入的身份資訊進行匹配。這一切都由我們自己來定義。

為什麼要用Realm

Shiro學習筆記(1)——shiro入門中,我們將身份資訊(使用者名稱/密碼/角色/許可權)寫在配置檔案中,但是實際開發中,這些身份資訊應該儲存在資料中,因此我們需要自定義Realm來從資料中獲取身份資訊,進行驗證。

自定義Realm

  • 定義一個MyRealm,繼承AuthorizingRealm
package com.shiro.realm;


import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import
org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import
org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.realm.Realm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyRealm1 extends AuthorizingRealm{ private static final transient Logger log = LoggerFactory.getLogger(Main.class); /** * 獲取身份資訊,我們可以在這個方法中,從資料庫獲取該使用者的許可權和角色資訊 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("----------doGetAuthorizationInfo方法被呼叫----------"); String username = (String) getAvailablePrincipal(principals); //我們可以通過使用者名稱從資料庫獲取許可權/角色資訊 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //許可權 Set<String> s = new HashSet<String>(); s.add("printer:print"); s.add("printer:query"); info.setStringPermissions(s); //角色 Set<String> r = new HashSet<String>(); r.add("role1"); info.setRoles(r); return info; } /** * 在這個方法中,進行身份驗證 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { //使用者名稱 String username = (String) token.getPrincipal(); log.info("username:"+username); //密碼 String password = new String((char[])token.getCredentials()); log.info("password:"+password); //從資料庫獲取使用者名稱密碼進行匹配,這裡為了方面,省略資料庫操作 if(!"admin".equals(username)){ throw new UnknownAccountException(); } if(!"123".equals(password)){ throw new IncorrectCredentialsException(); } //身份驗證通過,返回一個身份資訊 AuthenticationInfo aInfo = new SimpleAuthenticationInfo(username,password,getName()); return aInfo; } }
  • 讓我們定義的Realm起作用,就要在配置檔案中配置(shiro-realm.ini)
#宣告一個realm  
MyRealm1=com.shiro.realm.MyRealm1 
#指定securityManager的realms實現  
securityManager.realms=$MyRealm1
  • 測試
package com.shiro.realm;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {

    private static final transient Logger log = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        //獲取SecurityManager的例項
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");
        SecurityManager securityManager = factory.getInstance();

        SecurityUtils.setSecurityManager(securityManager);

        Subject currenUser = SecurityUtils.getSubject();

        //如果還未認證
        if(!currenUser.isAuthenticated()){
            UsernamePasswordToken token = new UsernamePasswordToken("admin","123");
            token.setRememberMe(true);
            try {
                currenUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("沒有該使用者: " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info( token.getPrincipal() + " 的密碼不正確!");
            } catch (LockedAccountException lae) {
                log.info( token.getPrincipal() + " 被鎖定 ,請聯絡管理員");
            }catch (AuthenticationException ae) {
                //其他未知的異常
            }
        }

        if(currenUser.isAuthenticated())
            log.info("使用者 "+currenUser.getPrincipal() +" 登入成功");

        //是否有role1這個角色
        if(currenUser.hasRole("role1")){
            log.info("有角色role1");
        }else{
            log.info("沒有角色role1");
        }
        //是否有對印表機進行列印操作的許可權
        if(currenUser.isPermitted("printer:print")){
            log.info("可以對印表機進行列印操作");
        }else {
            log.info("不可以對印表機進行列印操作");
        }
    }

}
  • 測試結果
    這裡寫圖片描述

從結果截圖中,我們可以看到,自定義的Realm中的doGetAuthorizationInfo 方法被呼叫了兩次,並且分別在currenUser.hasRole()currenUser.isPermitted 方法呼叫時呼叫

雜湊演算法支援

一般我們存入資料庫的密碼都是通過加密的,比如將“原密碼+鹽”進行一次或多次MD5計算,shiro提供了對雜湊演算法的支援

package com.shiro.realm;

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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class UserRealm extends AuthorizingRealm {

    private String salt = "hehe";//鹽

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {
        //使用者輸入的使用者名稱
        String username = (String) token.getPrincipal();
        //如果資料庫中沒有這個使用者,則返回null,登入失敗
        if(!username.equals("xiaozhou"))
            return null;

        //從資料庫中查詢密碼
        String password = "42029a889cc26562c986346114c02367";

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,
                password, ByteSource.Util.bytes(salt), getName());
        return info;
    }

}

使用MD5的realm和一般的realm沒有太多區別,唯一的區別在於:不使用雜湊演算法(即對密碼加密)的話,從資料庫查詢出來的密碼是明文,否則查詢出來的是密文,我們沒法使用密文來直接比對判斷密碼是否正確,為了讓shiro自動幫我們先加密再比對,我們要在配置檔案ini中告訴shiro使用什麼演算法

[main]
#密碼匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#匹配器使用md5
credentialsMatcher.hashAlgorithmName=md5
#進行幾次雜湊(用md5演算法做幾次運算)
credentialsMatcher.hashIterations=1

#realm
userRealm=com.shiro.realm.UserRealm
#該realm使用的匹配器是哪個
userRealm.credentialsMatcher=$credentialsMatcher
#使用哪個realm
securityManager.realms=$userRealm

多個Realm

  • 有時候,我們需要進行多次身份驗證,我們可以定義多個Realm,如同流水線一樣,shiro會依次呼叫Realm

  • MyRealm1

package com.shiro.mutilrealm;


import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.shiro.realm.Main;

public class MyRealm1 extends AuthorizingRealm{

    private static final transient Logger log = LoggerFactory.getLogger(Main.class);

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String username = (String) getAvailablePrincipal(principals);
        //通過使用者名稱從資料庫獲取許可權字串
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //許可權
        Set<String> s = new HashSet<String>();
        s.add("printer:print");
        s.add("printer:query");
        info.setStringPermissions(s);
        //角色
        Set<String> r = new HashSet<String>();
        r.add("role1");
        info.setRoles(r);

        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {
        log.info("MyRealm1開始認證。。。。。。");
        //使用者名稱
        String username = (String) token.getPrincipal();
        log.info("username:"+username);
        //密碼
        String password = new String((char[])token.getCredentials());
        log.info("password:"+password);
        //從資料庫獲取使用者名稱密碼進行匹配,這裡為了方面,省略資料庫操作
        if(!"admin".equals(username)){
            throw new UnknownAccountException();
        }
        if(!"123".equals(password)){
            throw new IncorrectCredentialsException();
        }
        //身份驗證通過
        AuthenticationInfo aInfo = new SimpleAuthenticationInfo(username,password,getName());

        return aInfo;
    }

}
  • MyRealm2和MyRealm1 程式碼其實基本上是一樣的,直接複製一份即可。當然,如果有需求,我們可以自由地定義修改Realm。這裡只做個示例而已。

配置Authenticator和AuthenticationStrategy

  • 這兩個東東是啥玩意?

    上面我們配置了多個Realm進行身份驗證,假設一下:MyRealm1 驗證通過了,MyRealm2驗證不通過怎麼辦,這就需要定義一個驗證策略來處理這種情況。Strategy的意思就是策略。Authenticator就是驗證器

  • 配置檔案(shiro-mutil-realm.ini)

#宣告一個realm  
MyRealm1=com.shiro.mutilrealm.MyRealm1
MyRealm2=com.shiro.mutilrealm.MyRealm2

#配置驗證器
authenticator = org.apache.shiro.authc.pam.ModularRealmAuthenticator
#配置策略
# AllSuccessfulStrategy 表示 MyRealm1和MyRealm2 認證都通過才算通過
authcStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy
#將驗證器和策略關聯起來
authenticator.authenticationStrategy = $authcStrategy
#配置驗證器所使用的Realm
authenticator.realms=$MyRealm2,$MyRealm1

#把Authenticator設定給securityManager
securityManager.authenticator = $authenticator

##########################################################################
# 1. AtLeastOneSuccessfulStrategy :如果一個(或更多)Realm 驗證成功,則整體的嘗試被認
# 為是成功的。如果沒有一個驗證成功,則整體嘗試失敗。

# 2. FirstSuccessfulStrategy 只有第一個成功地驗證的Realm 返回的資訊將被使用。所有進一步
# 的Realm 將被忽略。如果沒有一個驗證成功,則整體嘗試失敗

# 3. AllSucessfulStrategy 為了整體的嘗試成功,所有配置的Realm 必須驗證成功。如果沒有一
# 個驗證成功,則整體嘗試失敗。

# ModularRealmAuthenticator 預設的是AtLeastOneSuccessfulStrategy
###########################################################################
  • 驗證的策略有三種,在配置檔案中我用註釋都寫好了,就不再詳細說明了
  • 測試
package com.shiro.mutilrealm;

import java.util.List;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {

    private static final transient Logger log = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        //獲取SecurityManager的例項
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-mutil-realm.ini");
        SecurityManager securityManager = factory.getInstance();

        SecurityUtils.setSecurityManager(securityManager);

        Subject currenUser = SecurityUtils.getSubject();

        //如果還未認證
        if(!currenUser.isAuthenticated()){
            UsernamePasswordToken token = new UsernamePasswordToken("admin","123");
            token.setRememberMe(true);
            try {
                currenUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("沒有該使用者: " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info( token.getPrincipal() + " 的密碼不正確!");
            } catch (LockedAccountException lae) {
                log.info( token.getPrincipal() + " 被鎖定 ,請聯絡管理員");
            }catch (AuthenticationException ae) {
                //其他未知的異常
            }
        }

        if(currenUser.isAuthenticated())
            log.info("使用者 "+currenUser.getPrincipal() +" 登入成功");

        //得到一個身份集合
        PrincipalCollection principalCollection = currenUser.getPrincipals();

    }

}
  • 執行結果
    這裡寫圖片描述

結果很明顯,MyRealm1和MyRealm2依次執行

自定義AuthenticationStrategy(驗證策略)

  • 上面我們使用了shiro自帶的AuthenticationStrategy,其實我們也可以自己定義。
package com.shiro.authenticationstrategy;

import java.util.Collection;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AbstractAuthenticationStrategy;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.shiro.realm.Main;

public class MyAuthenticationStrategy extends AbstractAuthenticationStrategy{

    private static final transient Logger log = LoggerFactory.getLogger(MyAuthenticationStrategy.class);
    /**
     * 所有Realm驗證之前呼叫
     */
    @Override
    public AuthenticationInfo beforeAllAttempts(
            Collection<? extends Realm> realms, AuthenticationToken token)
            throws AuthenticationException {
        log.info("===============beforeAllAttempts方法被呼叫==================");
        return super.beforeAllAttempts(realms, token);
    }
    /**
     * 每一個Realm驗證之前呼叫
     */
    @Override
    public AuthenticationInfo beforeAttempt(Realm realm,
            AuthenticationToken token, AuthenticationInfo aggregate)
            throws AuthenticationException {
        log.info("===============beforeAttempt方法被呼叫==================");
        return super.beforeAttempt(realm, token, aggregate);
    }
    /**
     * 每一個Realm驗證之後呼叫
     */
    @Override
    public AuthenticationInfo afterAttempt(Realm realm,
            AuthenticationToken token, AuthenticationInfo singleRealmInfo,
            AuthenticationInfo aggregateInfo, Throwable t)
            throws AuthenticationException {
        log.info("===============afterAttempt方法被呼叫==================");
        return super.afterAttempt(realm, token, singleRealmInfo, aggregateInfo, t);
    }
    /**
     * 所有Realm驗證之後呼叫
     */
    @Override
    public AuthenticationInfo afterAllAttempts(AuthenticationToken token,
            AuthenticationInfo aggregate) throws AuthenticationException {
        log.info("===============afterAllAttempts方法被呼叫==================");
        return super.afterAllAttempts(token, aggregate);
    }


}

我們所繼承的 AbstractAuthenticationStrategy 中,各個方法並不是抽象的,也就是說並一定要重寫,我們可以根據需求重寫需要的方法即可

  • 配置檔案

    要讓我們自定義的AuthenticationStrategy起作用,只要將上面配置檔案(shiro-mutil-realm.ini)中
    authcStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy
    改為authcStrategy = com.shiro.authenticationstrategy.MyAuthenticationStrategy 即可

  • 測試程式碼不變

  • 結果
    這裡寫圖片描述

從截圖中也可以清除的看到自定義的策略中,各個方法被呼叫的順序。有了這些,我們就可以隨心所欲的根據需求進行操作了

多個Realm驗證順序

  • 隱式排列

    • 當你配置多個realm的時候,處理的順序預設就是你配置的順序。
    • 這種情況通常就是隻定義了realm,而沒有配置securityManager的realms
  • 顯式排列

    • 也就是顯示的配置securityManager.realms,那麼執行的順序就是你配置該值的realm的順序。
    • 通常更推薦顯示排列。

我們可以簡單的理解為,多個Realm驗證的順序,就是我們配置的順序