ApachShiro 一個系統 兩套驗證方法-(后台管理員登錄、前台App用戶登錄)同一接口實現、源碼分析


需求:

在公司新的系統里面博主我使用的是ApachShiro 作為安全框架、作為后端的鑒權以及登錄、分配權限等操作 管理員的信息都是存儲在管理員表

前台App 用戶也需要校驗用戶名和密碼進行登錄、但是用戶的信息卻是存在另一張表里面、如何給這兩個不同的數據表進行登錄?鑒權呢?

 

當然 按照Shiro的強大,我們完全可以用一個接口作為登錄的驗證、不同的Realm 來執行不同的邏輯即可

 

相關知識儲備 Realm 

 

 

用最簡單的話來說 一個Realm就是一個檢驗用戶身份的組件,但這里這個組件需要我們繼承去重寫,因為每個系統有各自不同的業務邏輯,這些事情是Shiro所不能了解的,我們得通過這個

Realm 告訴Shiro 我們的密碼是怎么加密得到的,還有用戶名是哪個,以及加密的方式是啥

 

————————————————————————————————————————————

 

了解這些需要了解的東西之后,我們模仿現有的Realm,照貓畫虎的來一個

@Component
public class WeChatRealm extends AuthorizingRealm {

    @Autowired
    private VehicleOwnerService vehicleOwnerService;

    @Autowired
    private SysUserService sysUserService;

    /**
     * 授權 微信接口沒有權限
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //沒有權限機制返回Null即可
        return null;
    }

    /*
     * @Author MRC
     * @Description 認證
     * @Date 11:36 2019/9/11
     * @Param [token]
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("微信登錄認證");

        //登錄用戶名
        String username = (String) token.getPrincipal();

        Wrapper<VehicleOwner> vehicleOwnerWrapper = new EntityWrapper<>();
        vehicleOwnerWrapper.eq("phone",username);

        VehicleOwner vehicleOwner = vehicleOwnerService.selectOne(vehicleOwnerWrapper);

        if (vehicleOwner == null) {
            //找不到這個用戶直接返回null
            return null;
        }

        //構造一個簡單的認證信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                vehicleOwner, //用戶名
                vehicleOwner.getPassword(), //密碼
                ByteSource.Util.bytes(username + vehicleOwner.getSalt()),//salt=username+salt
                getName()  //realm name
        );
        return authenticationInfo;
    }
}
  • 繼承  AuthorizingRealm 重寫  doGetAuthorizationInfo()鑒權方法 以及 doGetAuthenticationInfo()認證方法
  • 按照傳入的用戶名查找這個用戶是否存在,不存在就返回null即可
  • 這里不校驗密碼,直接把用戶名和密碼以及鹽值(如果存在加鹽機制)就一起傳遞過去 交給Shiro去校驗

 

加入到Shiro SecurityManager當中

    /**
     * 前端驗證Realm
     * @return
     */
    public WeChatRealm getWeChatRealm() {
        //使用MD5憑證管理器
        weChatRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return weChatRealm;
    }

 

通過@Bean 的方式注入一個SecurityManager 對象 並且加入多個Realm

 

@Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

//      securityManager.setRealm(myShiroRealm());
        List<Realm> list = new ArrayList<>();
        list.add(myShiroRealm());
        list.add(getWeChatRealm());

        //設置多個Realm
        securityManager.setRealms(list);
        // 自定義session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定義緩存實現 使用redis
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }

 

配置是配置好了 那他們兩個是如何工作的呢?

debug 走你~

 

從前台拿到的用戶名和密碼封裝成 token 傳遞到login方法內

        Subject subject = SecurityUtils.getSubject();
     //封裝toKen UsernamePasswordToken token
= new UsernamePasswordToken(sysUser.getUsername(), sysUser.getPassword()); //這里會拋出異常 subject.login(token);

 

 繼續跟進 ,進入用戶名校驗的過程。

 

 

token 里面封裝了我們傳遞過來的admin用戶名和密碼


繼續跟進,進入到login方法。 跳轉到authenticate(token)方法 這里才是真正意義上驗證方法

 

 

跟進到 authenticate(AuthenticationToken token) 方法 

 

 

 

doAuthenticate(token) 才是要進行驗證的方法,繼續跟進,進入到

 

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
      
    
     //初始化Realms  assertRealmsConfigured();
     //取出我們多個Realm Collection
<Realm> realms = getRealms();
     //一個或者多個執行不同的方法 
if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }

這里取出我們配置的兩個Realm

 

 

我們配置里兩個控制器,需要去做兩個不同的校驗,我們繼續跟進。

這里我把這個方法的源代碼貼出來,分析一下

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {

     //獲取驗證策略 AuthenticationStrategy strategy
= getAuthenticationStrategy();      
     //獲取到一個簡單的驗證信息(圖1) AuthenticationInfo aggregate
= strategy.beforeAllAttempts(realms, token); if (log.isTraceEnabled()) { log.trace("Iterating through {} realms for PAM authentication", realms.size()); }      
     //開始循環拿出所有的Realm  
for (Realm realm : realms) {        
       //這里返回的是傳入的 aggregate ,不知道這個是干嘛的(圖2) aggregate
= strategy.beforeAttempt(realm, token, aggregate);       
      
       //判斷是否支持toKen 
if (realm.supports(token)) { log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); AuthenticationInfo info = null; Throwable t = null; try {
            //關鍵 開始獲取驗證信息 開始用戶名和密碼的比對 info
= realm.getAuthenticationInfo(token); } catch (Throwable throwable) { t = throwable; if (log.isWarnEnabled()) { String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; log.warn(msg, t); } }           //驗證完成后 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } }      //將最終的驗證信息返回出去 圖7
     //如果aggregate(我們驗證的用戶信息)為空則拋出一個異常  aggregate
= strategy.afterAllAttempts(token, aggregate); return aggregate; }

 

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
     //獲取緩存里面的驗證信息     AuthenticationInfo info
= getCachedAuthenticationInfo(token); if (info == null) {

       //緩存里面沒有,開始驗證,跳轉到我們自己寫的邏輯 (圖3)  
//otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) {
          
        
         //這里把我們前台傳遞過來的token 以及從數據庫查詢出來的對象要進行一個對比     cacheAuthenticationInfoIfPossible(token, info); } }
else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) {
       //終於是密碼的比對  assertCredentialsMatch(token, info); }
else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); }      //密碼正確 返回info  return info; }

 

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
     ### 獲取密碼憑證管理器  CredentialsMatcher cm
= getCredentialsMatcher(); if (cm != null) {
       //檢驗密碼正確性(圖6)
if (!cm.doCredentialsMatch(token, info)) { //not successful - throw an exception to indicate this:
          //密碼錯誤,拋出異常
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }

 

 

圖一

在所有的嘗試之前,它先New 出一個簡單的驗證對象

 

圖二

返回的依舊是一個傳入的一個Aggregate對象

圖3 跳轉到我們自己寫的邏輯層 返回一個用戶名和密碼的包裝體

 

圖4 這里沒有開啟緩存,直接跳出,不走下面的緩存相關的方法

 

 圖5 拿到我們配置的憑證管理器,配置的MD5以及加密次數

 

 

圖6 檢驗密碼的正確性 使用equals方法進行比對兩個密碼的方法

 

 

第二遍循環,因為在第一個循環(第一個Realm)里面已經匹配到,第二個肯定匹配不到,我們繼續跟進


返回了一個NULL 

 

 

圖7

如果兩個都匹配不到,就會拋出一個異常,賬號不存在的異常,我們捕獲即可

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM