天天看點

Shiro - 認證那些事

【1】認證

① 身份認證

身份認證是第一道門戶,進去之後才能談論授權的問題。

身份驗證,一般需要提供如身份ID 等一些辨別資訊來表明登入者的身份,如提供email,使用者名/密碼來證明。

在shiro中,使用者需要提供principals (身份)和credentials(證明)給shiro,進而應用能驗證使用者身份:

  • principals:身份,即主體的辨別屬性,可以是任何屬性,如使用者名、郵箱等,唯一即可。一個主體可以有多個principals,但隻有一個Primary principals,一般是使用者名/郵箱/手機号。
  • credentials:證明/憑證,即隻有主體知道的安全值,如密碼/數字證書等。

最常見的principals 和credentials 組合就是使用者名/密碼了。

② 身份認證流程

1)收集使用者身份/憑證,即如使用者名/密碼;

2)調用Subject.login進行登入,如果失敗将得到相應的AuthenticationException異常,根據異常提示使用者錯誤資訊;否則登入成功;

第2)步細節如下:

  • 建立自定義的Realm 類,繼承org.apache.shiro.realm.AuthorizingRealm類,實作doGetAuthenticationInfo() 方法;
  • Shiro将會調用自定義的Realm 類的doGetAuthenticationInfo()方法,然後正常傳回或者抛出異常;

如下圖所示:

Shiro - 認證那些事

如下示例:

Shiro - 認證那些事

③ 認證過程詳解

步驟如下:

  • 首先調用Subject.login(token) 進行登入,其會自動委托給SecurityManager
  • SecurityManager負責真正的身份驗證邏輯;它會委托給Authenticator 進行身份驗證;
  • Authenticator 才是真正的身份驗證者,ShiroAPI 中核心的身份認證入口點,此處可以自定義插入自己的實作;
  • Authenticator 可能會委托給相應的AuthenticationStrategy進行多Realm 身份驗證,預設ModularRealmAuthenticator會調用AuthenticationStrategy進行多Realm 身份驗證;
  • Authenticator 會把相應的token 傳入Realm(通常是你的自定義CustomRealm.doGetAuthenticationInfo()),從Realm 擷取身份驗證資訊,如果沒有傳回/抛出異常表示身份驗證失敗了。此處可以配置多個Realm,将按照相應的順序及政策進行通路。
  • 密碼的比對:通過 AuthenticatingRealm 的 credentialsMatcher 屬性來進行的密碼的比對!

④ AuthenticationException

如果身份驗證失敗請捕獲AuthenticationException或其子類。最好使用如“使用者名/密碼錯誤”而不是“使用者名錯誤”/“密碼錯誤”,防止一些惡意使用者非法掃描帳号庫。

Shiro - 認證那些事

⑤ Realm

Shiro從Realm 擷取安全資料(如使用者、角色、權限),即SecurityManager要驗證使用者身份,那麼它需要從Realm 擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從Realm得到使用者相應的角色/權限進行驗證使用者是否能進行操作。

Realm接口如下:

String getName();//傳回一個唯一的Realm名字

boolean supports(AuthenticationToken token);//判斷此reaml是否支援此Token

AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws 
AuthenticationException;//根據Token擷取認證資訊      

一般繼承AuthorizingRealm(授權)即可;其繼承了AuthenticatingRealm(即身份驗證),而且也間接繼承了CachingRealm(帶有緩存實作)。

Realm 的繼承關系如下圖示:

Shiro - 認證那些事

⑥ Authenticator

Authenticator 的職責是驗證使用者帳号,是ShiroAPI 中身份驗證核心的入口點。如果驗證成功,将傳回AuthenticationInfo驗證資訊;此資訊中包含了身份及憑證;如果驗證失敗将抛出相應的AuthenticationException異常。

SecurityManager接口繼承了Authenticator接口,另外還有一個ModularRealmAuthenticator實作,其委托給多個Realm 進行驗證,驗證規則通過AuthenticationStrategy接口指定。

如下圖示所示:

Shiro - 認證那些事

Authenticator繼承關系圖如下:

Shiro - 認證那些事

⑦ AuthenticationStrategy

AuthenticationStrategy接口的預設實作:

  • FirstSuccessfulStrategy:隻要有一個Realm 驗證成功即可,隻傳回第一個Realm 身份驗證成功的認證資訊,其他的忽略;
  • AtLeastOneSuccessfulStrategy:隻要有一個Realm驗證成功即可,和FirstSuccessfulStrategy不同,将傳回所有Realm身份驗證成功的認證資訊;
  • AllSuccessfulStrategy:所有Realm驗證成功才算成功,且傳回所有Realm身份驗證成功的認證資訊,如果有一個失敗就失敗了。

如下圖所示:

Shiro - 認證那些事

ModularRealmAuthenticator預設是AtLeastOneSuccessfulStrategy政策。看不懂沒關系,繼續往下看。

⑦ 代碼示例

登入方法如下:

@RequestMapping(value="/doLogin" )
    public String doLogin(Model model, HttpSession session, HttpServletRequest request, HttpServletResponse response, String  userName, String password, String randomCode) throws Exception{
        String msg;
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        log.debug("UserNamePasswordToken----:"+JSON.toJSONString(token));
        token.setRememberMe(true);
        request.setAttribute("userName", userName);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            if (subject.isAuthenticated()) {
                return "redirect:/index";
            }
        } catch (UserNameException e) {
            msg = e.getMessage();//Password for account " + token.getPrincipal() + " was incorrect.
            model.addAttribute("message", msg);
        } catch (PasswordException e) {
            msg = e.getMessage();//Password for account " + token.getPrincipal() + " was incorrect.
            model.addAttribute("message", msg);
        } catch (IncorrectCredentialsException e) {
            msg = "登入密碼錯誤. ";//Password for account " + token.getPrincipal() + " was incorrect.
            model.addAttribute("message", msg);
        } catch (ExcessiveAttemptsException e) {
            msg = "登入失敗次數過多.";
            model.addAttribute("message", msg);
        } catch (LockedAccountException e) {
            msg = "帳号已被鎖定,如有疑問請聯系管理者. ";//The account for username " + token.getPrincipal() + " was locked.
            model.addAttribute("message", msg);
        } catch (DisabledAccountException e) {
            msg = "帳号已被禁用. ";//The account for username " + token.getPrincipal() + " was disabled.
            model.addAttribute("message", msg);
        } catch (ExpiredCredentialsException e) {
            msg = "帳号已過期. ";//the account for username " + token.getPrincipal() + "  was expired.
            model.addAttribute("message", msg);
        } catch (UnknownAccountException e) {
            msg = "帳号不存在. ";//There is no user with username of " + token.getPrincipal()
            model.addAttribute("message", msg);
        } catch (UnauthorizedException e) {
            msg = "您沒有得到相應的授權!" + e.getMessage();
            model.addAttribute("message", msg);
        }
        return "forward:/login";
    }      

自定義CustomRealm.doGetAuthenticationInfo()方法如下:

@Override
  protected AuthenticationInfo doGetAuthenticationInfo(
      AuthenticationToken authenticationToken) throws AuthenticationException {
      UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
          //擷取頁面傳來的使用者賬号
      String loginName = token.getUsername();
          //根據登入賬号從資料庫查詢使用者資訊
          SysUser user = sysUserService.getUserByLoginCode(loginName);
          System.out.println("從資料庫查詢到的使用者資訊 : "+user);
          //一些異常新娘西
          if (null == user) {
            throw new UnknownAccountException();//沒找到帳号
          }
          if (user.getStatus()==null||user.getStatus()==0) {
            throw new LockedAccountException();//帳号被鎖定
          }
          //其他異常...
          
          //傳回AuthenticationInfo的實作類SimpleAuthenticationInfo
          return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
  }      

需要注意的是在登入成功後,登出登入一定要走Shiro過濾器 logout:

<!--請求logout,shrio擦除sssion-->
 /logout=logout      

或者自定義登出過濾器,總之要完成如下的功能:任何現有的Session都将會失效,而且任何身份都将會失去關聯。在web應用程式中,RememberMe cookie也将被删除。

否則,雖然登出成功了,Shiro緩存中還認為該使用者認證成功,使用者通路需要認證的連接配接時,将會直接通過!

【2】多Realm認證與配置

多Realm認證需要将ModularRealmAuthenticator作為securityManager屬性注冊到容器中。

① 如下配置兩個自定義Realm:

<!-- 自定義Realm -->
    <bean id="customRealm" class="com.web.maven.shiro.CustomRealm">
        <!-- 将憑證比對器設定到realm中,realm按照憑證比對器的要求進行散列 -->
        <property name="credentialsMatcher">
           <bean  class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="MD5"/>
              <property name="hashIterations" value="1"/>
            </bean>
        </property>
    </bean>
      <!-- 自定義SecondRealm -->
    <bean id="customRealm2" class="com.web.maven.shiro.CustomRealm2">
        <!-- 将憑證比對器設定到realm中,realm按照憑證比對器的要求進行散列 -->
        <property name="credentialsMatcher">
           <bean  class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
              <property name="hashAlgorithmName" value="SHA1"/>
              <property name="hashIterations" value="1"/>
            </bean>
        </property>
    </bean>      

② 将兩個自定義Realm配置為bean-authenticator-class為ModularRealmAuthenticator的屬性

<bean id="authenticator" 
   class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
   <property name="realms">
         <list>
         <ref bean="customRealm"/>
         <ref bean="customRealm2"/>
      </list>
   </property>
</bean>      

③ 将配置的authenticator注入到securityManager中

<!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 注入緩存管理器 -->
        <property name="cacheManager" ref="cacheManager"/>
        <!--注入認證器-->
        <property name="authenticator" ref="authenticator"></property>
    </bean>      

④ 多Realm下就需要考慮上面說的認證政策了

在authenticator中配置如下:

<bean id="authenticator" 
        class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="realms">
            <list>
                <ref bean="customRealm"/>
                <ref bean="customRealm2"/>
            </list>
        </property>
        <!--ModularRealmAuthenticator預設認證政策即為AtLeastOneSuccessfulStrategy
        可不用顯示配置  -->
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
        </property>
    </bean>      

【3】多Realm配置方式2

上面是把realms配置給authenticator,當然也可以直接把realms配置給securityManager。

securityManager此時配置如下所示:

<!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 注入緩存管理器 -->
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"></property>
        <property name="realms">
            <list>
                <ref bean="jdbcRealm"/>
                <ref bean="secondRealm"/>
            </list>
        </property>
    </bean>      
<bean id="authenticator" 
      class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
  <property name="authenticationStrategy">
    <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
  </property>
</bean>      

繼續閱讀