天天看點

Shiro系列-Shiro如何實作身份驗證Shiro身份驗證入門小例子總結

導語

  下面就來按照順序依次介紹一下Shiro的使用場景,場景代碼後續會放到GitHub上面希望大家可以多多支援。首先先來介紹一下Shiro的身份認證。

文章目錄

  • Shiro身份驗證
  • 入門小例子
    • 環境準備
    • 登陸/退出操作
      • 1、準備使用者身份憑據
      • 編寫測試
    • 步驟總結
    • 身份認證流程
    • 檢視源碼
      • login(AuthenticationToken token) Subject的登陸方法
      • createSubject(token, info, subject) 建立一個Subject對象
      • onSuccessfulLogin(token, info, loggedIn) 登陸成功判斷
      • AbstractRememberMeManager 使用者管理器
  • 總結

Shiro身份驗證

身份驗證

  也就是說在應用中證明他就是它本人,在一般情況下例如使用者提供了一些身份驗證的資訊,用來辨別它本人,在Shiro中,使用者需要提供principals(身份)和credentials(證明)給Shiro,進而應用能驗證身份資訊。

principals

   身份,用來表明主體(Subject)辨別的屬性,可以是任意對象,例如使用者名、手機号等等,但是要有一點,這個辨別必須是唯一的,一個主體可以有多個principals,但是隻能有一個Primary principals,一般情況下選用唯一辨別。

credentials

  憑證,這個憑證類似于隻有使用者知道的一個安全碼,這個是每個使用者唯一的,類似于密碼安全證書等等。對于Shiro最常見的認證就是利用使用者名和密碼,也就是principals和credentials的組合來實作。

Realm

  安全驗證主體的資料源

入門小例子

環境準備

  這裡使用Maven建構工程,是以需要大家有一定的Maven基礎。當然也可以參考GitHub -support-shiro

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
    </dependencies>
           

登陸/退出操作

1、準備使用者身份憑據

  在resource目錄下面建立一個shiro.ini的檔案,内容如下,通過[users]指定了三個資料主體,nihui/123,test/123,admin/123。

[users]
nihui=123
test=123
admin=123
           

編寫測試

  進入到源碼目錄com.nihui.shiro.loginandlogout.LoginLogoutTest,中檢視測試類如下

public class LoginLogoutTest {
    public static void main(String[] args) {
        //1、擷取SecurityManager工廠,
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //2、得到一個SecurityManager執行個體,綁定到SecurityUtils
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        //得到Subject 以及使用者名密碼的身份驗證Token
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("nihui","123");
        // 驗證登陸

        try {
            subject.login(token);
        }catch (AuthenticationException e){
            //身份認證失敗
        }
        System.out.println(subject.isAuthenticated()); //true表示使用者已經登陸

        //退出操作
        subject.logout();

    }
}
           

  首先通過new IniSecurityManagerFactory()方法,指定一個ini配置檔案,來建立一個SecurityManager工廠;直接擷取SecurityManager執行個體并綁定到SecurityUtils,這個是一個全局的設定是以隻設定一次就可以了。

  完成綁定操作接下來就是認證操作,使用Subject綁定Token,通過login方法擷取登陸成功驗證。這裡需要重要的提示幾個異常操作,異常捕獲機制捕獲的是AuthenticationException類以及其子類,那麼它的子類有那些呢?

Shiro系列-Shiro如何實作身份驗證Shiro身份驗證入門小例子總結
  • DisabledAccountException :禁用賬号異常
  • LockedAccountException:鎖定賬号異常
  • UnknownAccountException:未知賬号異常
  • ExcessiveAttemptsException:登陸失敗次數過多異常
  • IncorrectCredentialsException:錯誤憑證,也就是密碼錯誤
  • ExpiredCredentialsException:過期憑證異常

具體使用執行個體

@RequestMapping(value = "/login",method = RequestMethod.POST)
public @ResponseBody String login(@RequestBody Admin admin, HttpSession httpSession, HttpServletRequest request){
     String flag = "false";
     String username = admin.getUsername();
     String password = admin.getPassword();

     if (StringUtils.isEmpty(username)||StringUtils.isEmpty(password)){
         flag = "使用者或者密碼為空";
         return JSON.toJSONString(flag);
     }
//        CustomerAuthenticationToken token = new CustomerAuthenticationToken(username,password,false);
     UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//        token.setLoginForm("1");
     System.out.println(token.getPassword());
     Subject currentUser = SecurityUtils.getSubject();

     System.out.println();
     try {
         logger.info("對使用者["+username+"]進行登陸驗證.驗證開始");
         currentUser.login(token);
         flag = "success";
         logger.info("對使用者["+username+"]進行登陸驗證.驗證通過");
     }catch (UnknownAccountException uae){
         logger.info("對使用者["+username+"]進行登陸驗證.驗證未通過,未知賬戶");
         flag = "未知賬戶";
     }catch (IncorrectCredentialsException ice){
         ice.printStackTrace();
         logger.info("對使用者["+username+"]進行登陸驗證.驗證未通過,錯誤的憑證");
         flag = "密碼不正确";
     }catch (LockedAccountException lae){
         logger.info("對使用者["+username+"]進行登陸驗證.驗證未通過,賬戶已鎖定");
         flag = "賬戶已鎖定";
     }catch (ExcessiveAttemptsException eae){
         logger.info("對使用者["+username+"]進行登陸驗證.驗證未通過,錯誤次數過多");
         flag = "使用者名或密碼錯誤次數過多";
     }catch (AuthenticationException ae){
         logger.info("對使用者["+username+"]進行登陸驗證.驗證未通過,堆棧軌迹如下");
         ae.printStackTrace();
         flag = "使用者名或密碼不正确";
     }

      //驗證是否成功
     if (currentUser.isAuthenticated()){
         Session session = SecurityUtils.getSubject().getSession();
         session.setAttribute("loginType","1");
         session.setTimeout(LOGIN_TIME_OUT);
         String ip = IpUtil.getIpAddr(request);
         //記錄登陸日志
         //logService.insertLoginLog(username,ip,request.getContextPath());
         return JSON.toJSONString(flag);
     }else {
         token.clear();
         return JSON.toJSONString(flag);
     }
}
           

步驟總結

  • 1、收集使用者身份憑證,例如使用者名密碼
  • 2、調用Subject.login方法進行登陸操作,如果失敗将會得到對應的異常,根據異常提示使用者錯誤資訊:或者是是否登陸成功
  • 3、調用Subject.logout退出系統

注意

  • 1、使用者名可以通過寫死的方式配置到ini檔案中,當然也可以存儲到資料庫中需要的時候從資料庫進行查詢操作,并且在存儲的時候使用者名密碼要進行加密處理
  • 2、使用者身份Token可能不僅僅可以用使用者名和密碼,也可以是手機号或者是郵箱,或者是多個驗證組合。

身份認證流程

Shiro系列-Shiro如何實作身份驗證Shiro身份驗證入門小例子總結

  通過上圖主要流程如下(按照圖中所标注的步驟)

主要流程

  • 1、調用 Subject.login進行登陸操作,将登陸委托給SecurityManager,在這之前必須通過SecurityUtils.setSecurityManager的設定。
  • 2、SecurityManager 負責真正實作身份驗證的邏輯,首先會委托給Authenticator進行驗證。
  • 3、Authenticator 作為真正的身份驗證者,是ShiroApi的入口點,在此可以進行自定義的設定。
  • 4、Authenticator會将身份驗證工作委托給AuthenticationStrategy,進行多個Realm身份驗證操作,預設ModularRealmAuthenticator 會調用AuthenticationStrategy進行多Realm身份驗證。
  • 5、Authenticator 會把相應的token傳入Realm,從Realm擷取身份驗證資訊,如果沒有傳回就會抛出異常身份驗證失敗,這裡可以設定多個Realm按照順序進行通路。

檢視源碼

  從上面内容可以知道,其實Subject就是應用于SecurityManager之間的代理,在org.apache.shiro.subject.support.DelegatingSubject 類中有Subject代理的具體實作内容。如下

login(AuthenticationToken token) Subject的登陸方法

public void login(AuthenticationToken token) throws AuthenticationException {
		//類似于清理緩存操作
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);
		//身份擷取
        PrincipalCollection principals;

        String host = null;
		//判斷是否實作的是預設的代理
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
            principals = delegating.principals;
            host = delegating.host;
        } else {
        	//如果不是則擷取到對應的自定義的使用者身份認證
            principals = subject.getPrincipals();
        }

        if (principals == null || principals.isEmpty()) {
            String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                    "empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
        
        this.principals = principals;
        this.authenticated = true;
        if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
            this.host = host;
        }
        Session session = subject.getSession(false);
        if (session != null) {
            this.session = decorate(session);
        } else {
            this.session = null;
        }
    }
           

  上面代碼到進入之後先完成了一個類似于清理的操作。然後回調了SecurityManager接口的login方法。這個方法的實際實作在org.apache.shiro.mgt.DefaultSecurityManager類中,具體内容如下

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    	//認證資訊
        AuthenticationInfo info;
        try {
        	//擷取到認證資訊
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            throw ae; //propagate
        }
		
        Subject loggedIn = createSubject(token, info, subject);

        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }
           

createSubject(token, info, subject) 建立一個Subject對象

上面代碼最為關鍵的地方就是,下面這個方法

Subject loggedIn = createSubject(token, info, subject);
           

這裡先來看一下createSubject()方法的實作

protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
        SubjectContext context = createSubjectContext();
        context.setAuthenticated(true);
        context.setAuthenticationToken(token);
        context.setAuthenticationInfo(info);
        if (existing != null) {
            context.setSubject(existing);
        }
        return createSubject(context);
    }
           

  最終經過一層一層的回調,實作了下面這個方法進而産生了一個Subject對象。然後繼續通過SecurityManager進行管理操作。

//since 1.2
    public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
                             Session session, boolean sessionCreationEnabled, SecurityManager securityManager) {
        if (securityManager == null) {
            throw new IllegalArgumentException("SecurityManager argument cannot be null.");
        }
        this.securityManager = securityManager;
        this.principals = principals;
        this.authenticated = authenticated;
        this.host = host;
        if (session != null) {
            this.session = decorate(session);
        }
        this.sessionCreationEnabled = sessionCreationEnabled;
    }
           

  也就是說,最終傳回的還是一個Subject對象,那麼接下來看一下onSuccessfulLogin(token, info, loggedIn);

onSuccessfulLogin(token, info, loggedIn) 登陸成功判斷

org.apache.shiro.mgt.AbstractRememberMeManager類中有如下一個方法。

protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
        rememberMeSuccessfulLogin(token, info, subject);
    }
           

  在上面方法中調用了

protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
        RememberMeManager rmm = getRememberMeManager();
        if (rmm != null) {
            try {
                rmm.onSuccessfulLogin(subject, token, info);
            } catch (Exception e) {
                if (log.isWarnEnabled()) {
                    String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
                            "] threw an exception during onSuccessfulLogin.  RememberMe services will not be " +
                            "performed for account [" + info + "].";
                    log.warn(msg, e);
                }
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("This " + getClass().getName() + " instance does not have a " +
                        "[" + RememberMeManager.class.getName() + "] instance configured.  RememberMe services " +
                        "will not be performed for account [" + info + "].");
            }
        }
    }
           

AbstractRememberMeManager 使用者管理器

/**
 * Abstract implementation of the {@code RememberMeManager} interface that handles
 * {@link #setSerializer(org.apache.shiro.io.Serializer) serialization} and
 * {@link #setCipherService encryption} of the remembered user identity.
 * <p/>
 * The remembered identity storage location and details are left to subclasses.
 * <h2>Default encryption key</h2>
 * This implementation uses an {@link AesCipherService AesCipherService} for strong encryption by default.  It also
 * uses a default generated symmetric key to both encrypt and decrypt data.  As AES is a symmetric cipher, the same
 * {@code key} is used to both encrypt and decrypt data, BUT NOTE:
 * <p/>
 * Because Shiro is an open-source project, if anyone knew that you were using Shiro's default
 * {@code key}, they could download/view the source, and with enough effort, reconstruct the {@code key}
 * and decode encrypted data at will.
 * <p/>
 * Of course, this key is only really used to encrypt the remembered {@code PrincipalCollection} which is typically
 * a user id or username.  So if you do not consider that sensitive information, and you think the default key still
 * makes things 'sufficiently difficult', then you can ignore this issue.
 * <p/>
 * However, if you do feel this constitutes sensitive information, it is recommended that you provide your own
 * {@code key} via the {@link #setCipherKey setCipherKey} method to a key known only to your application,
 * guaranteeing that no third party can decrypt your data.  You can generate your own key by calling the
 * {@code CipherService}'s {@link org.apache.shiro.crypto.AesCipherService#generateNewKey() generateNewKey} method
 * and using that result as the {@link #setCipherKey cipherKey} configuration attribute.
 *
 * @since 0.9
 */
           
  • 處理{@code remembermemanager}接口的抽象實作{@link setserializer(org.apache.shiro.io.serializer)序列化}和記住的使用者辨別的{@link setcipherservice encryption}。
  • 記住的辨別存儲位置和詳細資訊留給子類。預設加密密鑰,預設情況下,此實作使用{@link aescipherservice aescipherservice}進行強加密。它也使用預設生成的對稱密鑰來加密和解密資料。因為aes是對稱密碼,是以{@code key}用于加密和解密資料

注意

  因為shiro是一個開源項目,如果有人知道您使用的是shiro的預設值{@code key},他們可以下載下傳/檢視源代碼,并通過足夠的努力重新建構{@code key}随意解碼加密資料。當然,這個密鑰實際上隻用于加密記住的{@code principalCollection},它通常是使用者ID或使用者名。是以如果你不考慮這些敏感資訊,你認為預設的密鑰,那麼你可以忽略這個問題。但是,如果您認為這是敏感資訊,建議您提供自己的{@code key}通過{@link setcipherkey setcipherkey}方法指向一個隻有應用程式知道的密鑰,保證沒有第三方可以解密您的資料。您可以通過調用{@code cipherservice}的{@link org.apache.shiro.crypto.aescipherservice{generatenewkey()generatenewkey}方法并将該結果用作{@link setcipherkey cipherkey}配置屬性。

總結

  由于Shiro是開源的,是以為了安全起見,在使用的時候可以加入自己預設一些加密算法。是以說有時間還是要簡單的學習一下Shiro源碼有關的知識。了解其中提到的AES加密算法。或者是在使用的時候可以先對使用者名密碼進行加密操作。

繼續閱讀