Spring-shiro源碼陶冶-AuthorizingRealm用戶認證以及受權

閱讀源碼有助於陶冶情操,本文旨在簡單的分析shiro在Spring中的使用html

簡單介紹

Shiro是一個強大易用的Java安全框架,提供了認證、受權、加密和會話管理等功能java

AuthorizingRealm的繼承關係

Realm->CachingRealm->AuthenticatingRealm->AuthorizingRealm
本文只針對以上關係進行講解,其他的實現類請自行查看源碼web

Realm接口類

Realm提供了安全的訪問應用的相關實體類,好比用戶、角色、權限,對其中的訪問應用相應的認證或者受權操做。其提供的主要的方法爲AuthenticationInfo#getAuthenticationInfo,涉及的內容是關於信息的認證,這主要由AuthencatingRealm類實現算法

CachingRealm抽象類

提供緩存功能,簡單看下其下的變量以及主要方法。spring

  • 相關變量以下
//設置是否容許緩存,構造函數中默認爲true
    private boolean cachingEnabled;
    //設置緩存管理器,須要另外引入
    private CacheManager cacheManager;
  • 主要方法
  1. CachingRealm#afterCacheManagerSet
    默認實現爲空,供子類調用,在設置cacheManager變量調用此方法
  2. CachingRealm#getAvailablePrincipal
//獲取principal對象,通常都是子類在執行受權操做賦予的
protected Object getAvailablePrincipal(PrincipalCollection principals) {
        Object primary = null;
        if (!CollectionUtils.isEmpty(principals)) {
            Collection thisPrincipals = principals.fromRealm(getName());
            if (!CollectionUtils.isEmpty(thisPrincipals)) {
                primary = thisPrincipals.iterator().next();
            } else {
                //no principals attributed to this particular realm.  Fall back to the 'master' primary:
                primary = principals.getPrimaryPrincipal();
            }
        }

        return primary;
    }

AuthenticatingRealm-驗證Realm抽象類

簡單分析下其主要私有變量以及方法apache

  • 主要參數
//與父類的cachingEnabled結合使用,在構造函數裏其默認是false
private boolean authenticationCachingEnabled;
//用戶憑證驗證類,好比校驗密碼是否一致
private CredentialsMatcher credentialsMatcher;
//認證Token類,默認爲UsernamePasswordToken類
private Class<? extends AuthenticationToken> authenticationTokenClass;
  • 主要方法
  1. AuthenticatingRealm#setAuthenticationCachingEnabled-設置緩存是否許可
//可見若是隻設置authenticationCachingEnable,其爲true,也會設置cachingEnable=true
public void setAuthenticationCachingEnabled(boolean authenticationCachingEnabled) {
        this.authenticationCachingEnabled = authenticationCachingEnabled;
        if (authenticationCachingEnabled) {
            setCachingEnabled(true);
        }
    }
  1. AuthenticatingRealm#isAuthenticationCachingEnabled
//可見是否應用緩存是根據authenticationCachingEnabled和cachingEnabled聯合判斷的
public boolean isAuthenticationCachingEnabled() {
        return this.authenticationCachingEnabled && isCachingEnabled();
    }
  1. AuthenticatingRealm#getAuthenticationInfo
    認證接口實現方法,該方法的回調通常是經過subject.login(token)方法來實現的
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //獲取緩存中的認證信息,其中也會涉及到調用isAuthenticationCachingEnabled
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached, perform the lookup:
            //調用doGetAuthenticationInfo方法,此處爲抽象類,供子類調用
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                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);
        }

        return info;
    }
  1. AuthenticatingRealm#doGetAuthenticationInfo
    獲取認證信息方法,抽象方法供子類實現
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException ;
  1. AuthenticatingRealm#assertCredentialsMatch
    對憑證信息的校驗,涉及到加密方式
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        //獲取私有屬性credentialsMatcher
        CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) {
            //校驗方法
            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 {
            //可見credentialsMatcher屬性必需要設定,不然會拋異常
            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.");
        }
    }

AuthorizingRealm-受權抽象類

其中的內容和其父類驗證抽象類基本類似,這裏就不贅述了,主要提下主要方法api

  1. AuthorizingRealm#getAuthorizationInfo
//受權
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {

        if (principals == null) {
            return null;
        }

        AuthorizationInfo info = null;

        if (log.isTraceEnabled()) {
            log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
        }
        //是否引用cache
        Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
        if (cache != null) {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
            }
            Object key = getAuthorizationCacheKey(principals);
            info = cache.get(key);
            if (log.isTraceEnabled()) {
                if (info == null) {
                    log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
                } else {
                    log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
                }
            }
        }


        if (info == null) {
            // Call template method if the info was not found in a cache
            //調用受權抽象方法,供子類實現
            info = doGetAuthorizationInfo(principals);
            // If the info is not null and the cache has been created, then cache the authorization info.
            if (info != null && cache != null) {
                if (log.isTraceEnabled()) {
                    log.trace("Caching authorization info for principals: [" + principals + "].");
                }
                Object key = getAuthorizationCacheKey(principals);
                cache.put(key, info);
            }
        }

        return info;
    }
  1. AuthorizingRealm#doGetAuthorizationInfo
    真實受權抽象方法,供子類調用
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

示例

下述示例結合了受權與驗證,即繼承AuthorizingRealm便可瀏覽器

  1. spring-shiro配置文件樣例
<!--緩存管理器-->
<bean  id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>
    <!-- 憑證匹配器 -->
    <bean id="credentialsMatcher" class="com.jing.test.cas.admin.core.shiro.RetryLimitHashedCredentialsMatcher">
        <constructor-arg ref="cacheManager"/>
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="2"/>
        <property name="storedCredentialsHexEncoded" value="true"/>
    </bean>
    
    <!--密碼處理類-->
    <bean id="passwordService" class="com.jing.test.cas.admin.core.shiro.ShiroPasswordService"></bean>
    <!-- Realm實現 -->
    <bean id="shiroDBRealm" class="com.jing.test.cas.admin.core.shiro.ShiroDBRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
        <property name="cachingEnabled" value="false"/>
        <property name="shiroPasswordService" ref="passwordService"/>
        <property name="shiroUserService" ref="shiroUserService"/>
    </bean>
<!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="shiroDBRealm"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>
    <!-- 至關於調用SecurityUtils.setSecurityManager(securityManager) -->
    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
        <property name="arguments" ref="securityManager"/>
    </bean>
<!-- Shiro的Web過濾器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/index.html"/>
        <property name="unauthorizedUrl" value="/403.html"/>
       <property name="filters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
                <entry key="perm" value-ref="permissionsAuthorizationFilter"/>
                <entry key="captcha" value-ref="captchaValidateFilter"/>
                <entry key="sysUser" value-ref="sysUserFilter"/>
                <entry key="user" value-ref="userFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <!-- 若是不該用acm cas方案則添加  /logout=logout -->
            <value>
                /ossmanager/api/** = anon
                /test/** = anon
                /login = captcha,authc
                /logout=logout
                /index = anon
                /jcaptcha.jpeg = anon
                /403.html = anon
                /login.html = anon
                /favicon.ico = anon
                /static/** = anon
                /index.html=user,sysUser
                /welcome.html=user,sysUser
                /admin/user/modifyPwd.html=user,sysUser
                /admin/user/updatePassword=user,sysUser
                /admin/user/role/list=user,sysUser
                /** = user,sysUser,perm
            </value>
        </property>
    </bean>

    <!-- Shiro生命週期處理器-->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
  1. Realm接口實現類
package com.jing.test.cas.admin.core.shiro;

import com.jing.test.cas.admin.core.exceptions.BizException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Set;
/**
 *Realm接口實現類
 */
public class ShiroDBRealm  extends AuthorizingRealm {
    /**
     * 日誌對象
     */
    private static final Logger logger = LoggerFactory.getLogger(ShiroDBRealm.class);

    /**
     * 帳戶禁用
     */
    private static final String USER_STATUS_FORBIDDEN = "1";

    /**
     * 權限相關用戶服務接口
     */
    private IShiroUserService shiroUserService;

    /**
     * 密碼服務類 加密做用
     */
    private ShiroPasswordService shiroPasswordService;

    /**
     * 受權
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 由於非正常退出,即沒有顯式調用 SecurityUtils.getSubject().logout()
        // (多是關閉瀏覽器,或超時),但此時緩存依舊存在(principals),因此會本身跑到受權方法裏。
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            doClearCache(principalCollection);
            SecurityUtils.getSubject().logout();
            return null;
        }
        ShiroUser shiroUser = (ShiroUser)principalCollection.getPrimaryPrincipal();
        String userName = shiroUser.getUserName();
        if(StringUtils.isNotBlank(userName)){
            SimpleAuthorizationInfo sazi = new SimpleAuthorizationInfo();
            try {
                Set<String> roleIds= shiroUserService.getRoles(userName);
                for (String roleId: roleIds){
                    shiroUser.setRoleId(roleId);
                }
                sazi.addRoles(roleIds);
                sazi.addStringPermissions(shiroUserService.getPermissions(userName));
                return sazi;
            } catch (Exception e) {
                logger.error(e.getMessage(),e);
            }
        }
        return null;
    }

    /**
     * 認證
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        ShiroUser shiroUser = shiroUserService.getShiroUser(token.getUsername());
        checkUserStatus(shiroUser);
        if(shiroUser != null){
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(shiroUser,shiroUser.getPassword(),ByteSource.Util.bytes(shiroUser.getUserName()+shiroPasswordService.getPublicSalt()),getName());
            return authenticationInfo;
        }
        return null;
    }

    /**
     * 檢查用戶狀態
     * @param shiroUser
     */
    private void checkUserStatus(ShiroUser shiroUser) {
        if(StringUtils.equalsIgnoreCase(shiroUser.getUserStatus(),USER_STATUS_FORBIDDEN)){
            throw new ForbiddenException("用戶已被禁用");
        }
    }

    /**
    * 初始化方法
     * 設定Password校驗的Hash算法與迭代次數.
     **/
    public void initCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(
                shiroPasswordService.getHashAlgorithm());
        matcher.setHashIterations(shiroPasswordService.getHashInterations());
        setCredentialsMatcher(matcher);
    }

    public void setShiroUserService(IShiroUserService shiroUserService) {
        this.shiroUserService = shiroUserService;
    }

    public void setShiroPasswordService(ShiroPasswordService shiroPasswordService) {
        this.shiroPasswordService = shiroPasswordService;
    }
}

總結

受權與驗證的邏輯作了簡單的瞭解,其中受權可經過subject.isPermited()等方法調用;驗證則可經過subject.login()等方法來調用緩存

相關文章
相關標籤/搜索