1. 程式人生 > 程式設計 >shiro 原始碼閱讀心得

shiro 原始碼閱讀心得

有些圖片看不清,GitHub裡面有:github.com/nice01qc/sh… GitHub資源已經上傳

shiro介紹(個人理解)

    shiro 就是管理使用者許可權的一個框架。就是用來管理和驗證使用者的角色以及每個角色有哪些許可權,功能很簡單,但是shiro把這個搞成了一個擴充套件性很強的框架,就是驗權和認證的一種規範,具體實現就是配置shiro。

    下面是我閱讀過程,和一些小總結,且原始碼可以執行。如果把shiro比作一個武士,SecurityManager就是這個武士的刀,shiro Filter 過濾器就是武士這個人,一個是工具,一個是使用方。可見這兩個方面都很重要,本文提供這兩個方面的解釋和一個原始碼閱讀過程,寫的有些粗糙,但是如果你想閱讀shiro原始碼,GitHub裡面有配置好的原始碼,裡面有現成的springboot例子和一個單點登入模擬。

如果你認真學完這個,將獲得技能有:

  1. 可以輕鬆解決所有有關shiro的配置(如 springmvc 怎麼配置shiro,springboot 怎麼配置shiro,單點登入怎麼配置........)
  2. 對許可權管理系統會有進一步理解
  3. 加深對框架設定本身的理解

專案整體介紹:

​ 整個專案克隆自shiro官方倉庫,只是在子專案 samples裡面添加了2個小專案,用於debug。

  • shiro_learn(專案根路徑)
    • samples
      • shiro_cas_service 使用springboot簡單模擬cas服務(通過debug就可以完全瞭解cas服務請求過程)
      • shiro_client 原始碼分析開始的地方,shiro大部分配置已經配置好了

本文將從三個個方面進行分析:

  1. shiro配置(很全的)
  2. Shiro web 過濾器 載入過程
  3. 完整分析一個shiro cas請求

shiro 配置


上圖便是shiro所有功能展示圖,從上圖可以看出主要分為2大塊,

​ 一塊是Security Manager ,這個模組及授權驗證於一身(對應類為:SecurityManager 的子類),並通過Subject介面對外提供服務,例如登入、驗權等等使用subject就好了(具體使用,後面會詳細分析);

​ 另一塊就是使用方,就是SecurityManager 已經配置好了,第三方應該如何使用呢,本文以Web MVC 這個第三方來敘述,Web MVC

主要通過 Filter 過濾器攔下所有使用者請求,然後分發給相應的 shiro Filter進行相應的處理(例如看看這個請求有沒有登入,有沒有許可權),在Filter中 使用 SecurityManager 提供的 Subject介面 進行相應的操作。

SecurityManager配置

通過上圖可以看出DefaultSecurityManager可以配置的屬性有哪些都可以看出來,下面通過表格一一說明:

屬性 是否存在預設配置 預設配置類 作用說明 常用配置
subjectFactory DefaultSubjectFactory Subject的具體實現類(對外服務介面,如果是cas服務,建議使用CasSubjectFactory覆蓋)
subjectDAO DefaultSubjectDAO 主要用於將subject中最新資訊儲存到session裡面
rememberMeManager 只存在DefaultWebSecurityManager中 CookieRememberMeManager 用於管理rememberMe這個cookie,一般不用
sessionManager DefaultSessionManager (DefaultWebSecurityManager 就是DefaultWebSessionManager) 有關session的操作最終都會委託給他做(他本身還可以配置,見下表)
authorizer ModularRealmAuthorizer 授權策略(多個realm時,可以設定自己的策略)
authenticator ModularRealmAuthenticator 認證策略(多個realm時可以設定自己的策略)
realm CasRealm、JdbcRealm ...... 落實認證和授權操作,需要自己配置(後面會舉個例子細講)
cacheManager 使用者最好繼承AbstractCacheManager這個抽象類(支援shiro預設週期管理) 在realm認證和授權的時候會用到(相當於加了一層快取,cas認證就不需要了,如果是使用者名稱密碼,可以使用,增加認證授權速度) 否(cas就不需要)

DefaultSecurityManager的sessionManager (DefaultSessionManager)屬性配置:

屬性 是否存在預設配置 預設配置類 作用說明 常用配置
sessionFactory SimpleSessionFactory 用於建立Session的,一般不配置
sessionDAO MemorySessionDAO 用於儲存Session的介面,一般通過繼承AbstractSessionDAO類,並使用Redis重新配置一個,將Session儲存在redis裡面(此抽象類可以配置自己的SessionIdGenerator => 用於產生session id的)
casheManager 可以跟實現了CashManagerAware 這個類 的sessionDAO 聯合使用,一般就配置sessionDAO就完事
sessionIdCookie new SimpleCookie("JSESSIONID"); 這個存在於本類的子類DefaultWebSessionManager中,一般都重新配置,這樣cookie名字可以改成自己的(一般有關cookie底層的操作都委託給他來做,例如讀取此cookie的值,配置cookie ......)

注:這些屬性通過物件的set方法都可以設定

Web MVC過濾器配置

​ DefaultWebSecurityManager 跟DefaultSecurityManager 配置一樣,以下說的securityManager 就是指DefaultWebSecurityManager 。

​ 現在DefaultWebSecurityManager 已經配置好了,這個東西應該放在那裡呢,肯定得應用到shiro過濾器裡面去。

​ shiro 過濾器相關配置存放在ShiroFilterFactoryBean這個類裡面,然後使用DelegatingFilterProxyShiroFilterFactoryBean注入到web容器裡。

  • 如果是springmvc,直接在web.xml內配置這個DelegatingFilterProxy就好,過濾器名字就是ShiroFilterFactoryBean這個bean的名字;
  • 如果是springboot,使用springboot的 FilterRegistrationBean進行註冊就好(servlet3.0可以使用ServletContext進行註冊,可以註冊servlet、filter......) => 後續會細講

ShiroFilterFactoryBean ,通過名字就可以看出來,他不是真正的Filter,他使用來配置和管理shirofilter的。

從上圖可以看出來,通過FactoryBean 來返回SpringShiroFilter這個物件。ShiroFilterFactoryBean 配置說明如下:

屬性 作用說明
securityManager shiro核心 ......
filters (Map<String,Filter>) 將自定義的shiro Filter都放在這個map裡面,一般與下面這個定義聯合使用
filterChainDefinitionMap (Map<String,String>) Map<請求url表示式,用到的過濾器名字(多個用逗號隔開)> (原文解釋:urlPathExpression_to_comma-delimited-filter-chain-definition)
loginUrl 登入URL
successUrl 登入成功後跳轉的URL(一般不會使用這個跳轉,而使使用第一次訪問時儲存的url)
unauthorizedUrl 授權不成功跳轉的URL

以上所有配置例子 放在 shiro_learn/samples/shiro_clien/src/main/java/com/nice01qc/config/shiro/ShiroCasConfig.java 這個類裡面,可以對應著看。

Shiro web 過濾器 載入過程

這個載入過程其實就是 ShiroFilterFactoryBean 載入過程

​ 現在securityManager已經手動配置好了,沒什麼好說的。let's coding .......,所有原始碼,只指出比較關鍵的節點,具體細節自己落實。

ShiroFilterFactoryBean.java 載入過程

public class ShiroFilterFactoryBean implements FactoryBean,BeanPostProcessor {
    
	// 就是從這個地方開始(如果不知道,請搜尋 FactoryBean的作用)
    public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();  //直接看這個就好
        }
        return instance;
    }
    
 
    protected AbstractShiroFilter createInstance() throws Exception {
        SecurityManager securityManager = getSecurityManager();
        // 封裝Filter呼叫管理,繼續深入
        FilterChainManager manager = createFilterChainManager();
        
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
        
        //此處建立真正的 總覽全域性的 shiro filter
        return new SpringShiroFilter((WebSecurityManager) securityManager,chainResolver);
    }
    
    protected FilterChainManager createFilterChainManager() {
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        // 獲取預設Filter ,取自DefaultFilter列舉類(共12個)
        Map<String,Filter> defaultFilters = manager.getFilters(); 
        // 將loginUrl、successUrl、unauthorizedUrl填充到符合要求的 filter 內
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }

        //這是你自己定義的Filter
        Map<String,Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String,Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter); // 填充一波
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                //'init' argument is false,since Spring-configured filters should be initialized
                //in Spring (i.e. 'init-method=blah') or implement InitializingBean:
                manager.addFilter(name,filter,false);
            }
        }

        //build up the chains:
        Map<String,String> chains = getFilterChainDefinitionMap(); // filterChainDefinitionMap
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String,String> entry : chains.entrySet()) {
                // 例如 filterChainDefinitionMap.put("/index","authc[config1,config2]");
                String url = entry.getKey();	// url 就是 "/index"
                String chainDefinition = entry.getValue(); // "authc[config1,config2]"
                // 會將 chainDefinition的“[config1,config2]” 解析後封裝在 authc對應的filter內部,後續會用到
                manager.createChain(url,chainDefinition);
            }
        }
        return manager;
    }
    
    // 填充那三個值的地方
    private void applyGlobalPropertiesIfNecessary(Filter filter) {
        applyLoginUrlIfNecessary(filter);
        applySuccessUrlIfNecessary(filter);
        applyUnauthorizedUrlIfNecessary(filter);
    }
    private void applyLoginUrlIfNecessary(Filter filter) {
        String loginUrl = getLoginUrl();
        if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
            AccessControlFilter acFilter = (AccessControlFilter) filter;
            String existingLoginUrl = acFilter.getLoginUrl();
            if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
                acFilter.setLoginUrl(loginUrl);
            }
        }
    }
}
複製程式碼

以上就是ShiroFilterFactoryBean類初始化過程,後續DelegatingFilterProxy通過getBean("ShiroFilterFactoryBean的bean name") 來獲取這個bean,並將請求委託給他。

shiro Filter繼承體系詳細介紹

在繼續深入之前,先介紹下shiro filter的特色,就是你想實現不同功能的filter,通過繼承shiro自帶的Filter就好,然後在這基礎之上再做修改。

以上每一個抽象類都有不同的作用,分工明確,下面一個一個來分析(上面的loginUrl在上面初始化的時候就注入進去了):=> 這個很關鍵

AbstractFilter.java

// 實現了Filter的init介面,並對外暴露了onFilterConfigSet 介面
public abstract class AbstractFilter extends ServletContextSupport implements Filter {
    
    // 實現了Filter的init介面,並對外暴露了onFilterConfigSet 介面,這個介面在AbstractShiroFilter中覆蓋了這個方法,其中AbstractShiroFilter 是 SpringShiroFilter的父類喔,SpringShiroFilter的父類喔,SpringShiroFilter的父類喔
    public final void init(FilterConfig filterConfig) throws ServletException {
        setFilterConfig(filterConfig);
        try {
            onFilterConfigSet();
        } catch (Exception e) {
        }
    }
   
    public void destroy() {
    }
    
}
複製程式碼

NameableFilter.java

public abstract class NameableFilter extends AbstractFilter implements Nameable { 
    // 設定filter 名字用的
	public void setName(String name) {
        this.name = name;
    }
}
複製程式碼

OncePerRequestFilter.java

public abstract class OncePerRequestFilter extends NameableFilter {
    // 保證每次請求只訪問一次
    public final void doFilter(ServletRequest request,ServletResponse response,FilterChain filterChain) throws ServletException,IOException {
        // 檢視是否已經訪問過一次
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        // 如果訪問了一次,則跳過這個filter,繼續下一個
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            filterChain.doFilter(request,response);
        } else if (!isEnabled(request,response) || shouldNotFilter(request) ) {
            filterChain.doFilter(request,response);
        } else {
            // 沒有訪問,現在標記
            request.setAttribute(alreadyFilteredAttributeName,Boolean.TRUE);
            try {
               	// 執行本filter 內容
                doFilterInternal(request,response,filterChain);
            } finally {
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }
    
    // 子類需要實現的介面,不需要實現doFilter這個方法,通過實現這個方法,可以幹更多事
    protected abstract void doFilterInternal(ServletRequest request,FilterChain chain) throws ServletException,IOException;
    
}
複製程式碼

AdviceFilter.java

public abstract class AdviceFilter extends OncePerRequestFilter {
    
    // 實現了OncePerRequestFilter這個方法,在這個方法有點像aop的風格
    public void doFilterInternal(ServletRequest request,FilterChain chain)
            throws ServletException,IOException {
        try {
		   // 在執行之前,先執行 preHandle 方法
            boolean continueChain = preHandle(request,response);
		   // 如果 preHandle 沒通過,將不再繼續往下執行
            if (continueChain) {
                executeChain(request,chain);
            }
            // 執行之後再執行的方法
            postHandle(request,response);
        } catch (Exception e) {
            exception = e;
        } finally {
            cleanup(request,exception);
        }
    }
    // 將這個方法暴露出去
    protected boolean preHandle(ServletRequest request,ServletResponse response) throws Exception {
        return true;
    }
    // 將這個方法也暴露出去
    protected void postHandle(ServletRequest request,ServletResponse response) throws Exception {
    }      
}
複製程式碼

PathMatchingFilter.java

// 用於判斷請求是否符合本 Filter,只有請求URL 跟本Filter對應的url匹配規則對上
public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
    // ShiroFilterFactoryBean 初始化時就填充進去了,
    // 裡面value就是那個autho[config1,config2] 中這個[config1,config2]陣列
    protected Map<String,Object> appliedPaths = new LinkedHashMap<String,Object>();
    	// 覆蓋了父類的preHandle方法,首先判斷請求url是否匹配 本Filter的appliedPaths
        protected boolean preHandle(ServletRequest request,ServletResponse response) throws Exception {
		// 如果Filter本身沒有匹配url,返回true
        if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
            return true;
        }

        for (String path : this.appliedPaths.keySet()) {
            //(first match 'wins'):
            if (pathsMatch(path,request)) {
                Object config = this.appliedPaths.get(path);
                // 如果匹配上了就執行這個方法,它會進一步交給onPreHandle方法,並對外暴露這個方法
                return isFilterChainContinued(request,path,config);
            }
        }

        //no path matched,allow the request to go through:
        return true;
    }

	private boolean isFilterChainContinued(ServletRequest request,String path,Object pathConfig) throws Exception {

        if (isEnabled(request,pathConfig)) { //isEnabled check added in 1.2

            return onPreHandle(request,pathConfig);
        }
        return true;
    }
    
   // 對外暴露此介面
	protected boolean onPreHandle(ServletRequest request,Object mappedValue) throws Exception {
        return true;
    }    
}
複製程式碼

AccessControlFilter.java(如果你要驗證授權什麼的,這個類還是比較關鍵的)

// 此介面用於判斷請求是否可以通過,不通過就跳登入,符合要求就使用subject進行登入 等等
public abstract class AccessControlFilter extends PathMatchingFilter {
    // 覆蓋父類PathMatchingFilter 暴露的方法,並在方法內添加了兩種方法
    // 一個是isAccessAllowed,用於判斷是否已經驗證過了,例如使用者已經登入過了有session
    // 另一個是 onAccessDenied 驗證失敗,失敗後幹嘛,鬼知道,你看他怎麼實現的
     public boolean onPreHandle(ServletRequest request,Object mappedValue) throws Exception {
        return isAccessAllowed(request,mappedValue) || onAccessDenied(request,mappedValue);
    }
    
    // 直接暴露給外界,自己看著實現吧,可別把onAccessDenied的活也幹了就好了
    protected abstract boolean isAccessAllowed(ServletRequest request,Object mappedValue) throws Exception;
    
    // 這個老哥,提供了兩個方式供外界覆蓋,也就是方法 過載,就是看你要不要那個引數
    protected boolean onAccessDenied(ServletRequest request,Object mappedValue) throws Exception {
        return onAccessDenied(request,response);
    }
    // .......
    protected abstract boolean onAccessDenied(ServletRequest request,ServletResponse response) throws Exception;
    
    // 判斷是不是登入請求
    protected boolean isLoginRequest(ServletRequest request,ServletResponse response) {
        return pathsMatch(getLoginUrl(),request);
    }
    // 儲存請求並重定向到登入頁面
    protected void saveRequestAndRedirectToLogin(ServletRequest request,ServletResponse response) throws IOException {
        saveRequest(request);
        redirectToLogin(request,response);
    }
    
    protected void saveRequest(ServletRequest request) {
        WebUtils.saveRequest(request);
    }
    
    protected void redirectToLogin(ServletRequest request,ServletResponse response) throws IOException {
        String loginUrl = getLoginUrl();
        WebUtils.issueRedirect(request,loginUrl);
    }
        
}
複製程式碼
以上做個小總結:
  1. 如果你就想寫個簡單的Filter,請直接實現Filter介面
  2. 如果你想給Filter搞個名字,請繼承NameableFilter抽象類
  3. 如果你想保證你的filter只被呼叫了一次,請繼承OncePerRequestFilter抽象類
  4. 如果你想你的filter在dofilter 方法前後(類似這個方法的aop),請繼承AdviceFilter(看著這個Advice是不是特別熟悉-----spring aop也有advice這個概念)
  5. 如果你想寫個驗證和授權的Filter,請繼續往下看,因為有兩種實現了AccessControlFilter的類(輪子已經建好,上車吧)

一種是 認證類的(Authenticate):

AuthenticationFilter.java (一般以ion結尾的單詞類,一般提供很基礎的服務)

public abstract class AuthenticationFilter extends AccessControlFilter {
    	// 提供基礎的驗證
        protected boolean isAccessAllowed(ServletRequest request,Object mappedValue) {
        Subject subject = getSubject(request,response);
        return subject.isAuthenticated() && subject.getPrincipal() != null;
    }
    
        protected void issueSuccessRedirect(ServletRequest request,ServletResponse response) throws Exception {
            // 通過驗證後,會被重定向到自己設定好的url,這個可以改
        WebUtils.redirectToSavedRequest(request,getSuccessUrl());
    }
    
}
複製程式碼

AuthenticatingFilter.java(真正幹活的,以後認證什麼的繼承他就好了,稍微修改就好了)

public abstract class AuthenticatingFilter extends AuthenticationFilter {
    
    // 覆蓋了,父類的方法,並在這個方法內部添加了vip功能(可以使用isPermissive走vip通道)
    // 如果是 AuthorizationFilter 的這個方法,mappedValue就是用於角色驗證了,預設是所有角色都必須通過,可以覆蓋這個方法
        @Override
    protected boolean isAccessAllowed(ServletRequest request,Object mappedValue) {
        return super.isAccessAllowed(request,mappedValue) ||
                (!isLoginRequest(request,response) && isPermissive(mappedValue));
    }
    
    // 這是vip模板,僅僅是vip,普通乘客就別走這個了
    protected boolean isPermissive(Object mappedValue) {
        if(mappedValue != null) {
            String[] values = (String[]) mappedValue;
            return Arrays.binarySearch(values,PERMISSIVE) >= 0;
        }
        return false;
    }
    
    
    // 這個方法一般提供給onAccessDenied 方法的,就是你沒通過認證,應該登入認證一波了
    protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception {
        AuthenticationToken token = createToken(request,response);
        if (token == null) {
            throw new IllegalStateException(msg);
        }
        try {
            Subject subject = getSubject(request,response);
            subject.login(token);
            return onLoginSuccess(token,subject,request,response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token,e,response);
        }
    }
    
    // 對外暴露介面,用於生成token,因為登入必須拿著token去登入,預設提供了兩種生成token的方法,就在這個方法下面喔
    protected abstract AuthenticationToken createToken(ServletRequest request,ServletResponse response) throws Exception;
}
複製程式碼

**最後來一個例子: **

一個shiro Filter示例介紹

CasFilter.java (來看看cas他幹了什麼)

public class CasFilter extends AuthenticatingFilter {
    // 直接覆蓋父類這個方法,意思就是,你竟然遇到了我,那你就是沒有認證(需要被安排下)
        @Override
    protected boolean isAccessAllowed(ServletRequest request,Object mappedValue) {
        return false;
    }
    
    // 妥妥的實現這個方法,帶我去登入吧,我準備好了
        @Override
    protected AuthenticationToken createToken(ServletRequest request,ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String ticket = httpRequest.getParameter(TICKET_PARAMETER);
        return new CasToken(ticket);
    }
    
    // 去吧皮卡丘,送你去登入
        @Override
    protected boolean onAccessDenied(ServletRequest request,ServletResponse response) throws Exception {
        // 所以casFilter 直接執行這一步,前面那些方法都沒起作用
        return executeLogin(request,response);
    }
    
}
複製程式碼

授權類就不說了(同上),下面是上面Filter的一些預設實現(DefaultFilter列舉類中可以看到)(是不是很眼熟)

filter名字 對應的Filter
anon AnonymousFilter.java
authc FormAuthenticationFilter.java
authcBasic BasicHttpAuthenticationFilter.java
authcBearer BearerHttpAuthenticationFilter.java
logout LogoutFilter.java
noSessionCreation NoSessionCreationFilter.java
perms PermissionsAuthorizationFilter.java
port PortFilter.java
rest HttpMethodPermissionFilter.java
roles RolesAuthorizationFilter.java
ssl SslFilter.java
user UserFilter.java

通過以上Filter的瞭解,你是否已經瞭解shiro系Filter,如果不理解,請再看一遍(這是死迴圈判斷,除非已經看懂,否則不會break;)容器初始化講完了,是時候來個請求了。

完整分析一個shiro 請求

​ 那就從請求被Filter攔截那個地方說起吧,到底被誰攔截了,是DelegatingFilterProxy這個類,好了,就在這個類的doFilter方法上打個斷點(打斷點前順便看下Filter的init() 方法吧)let's begin ......

DelegatingFilterProxy.java (請求開始的地方)

public class DelegatingFilterProxy extends GenericFilterBean {
        
    // 這個方法是在Filter init時呼叫的,在啟動服務過程就會呼叫,他會初始化delegate,而這個delegate就是ShiroFilterFactoryBean
    @Override
	protected void initFilterBean() throws ServletException {
		synchronized (this.delegateMonitor) {
			if (this.delegate == null) {
				// If no target bean name specified,use filter name.
                // 什麼是targetBeanName 見 DelegatingFilterProxy建構函式
				if (this.targetBeanName == null) {
					this.targetBeanName = getFilterName();
				}

				WebApplicationContext wac = findWebApplicationContext();
				if (wac != null) {
					this.delegate = initDelegate(wac); // 直接看這個
				}
			}
		}
	}
    
    // 看這個就好了
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		String targetBeanName = getTargetBeanName();
        // 直接通過ApplicationContex直接getBean了
		Filter delegate = wac.getBean(targetBeanName,Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}
   
   // targetBeanName的由來
   public DelegatingFilterProxy(String targetBeanName,@Nullable WebApplicationContext wac) {
		this.setTargetBeanName(targetBeanName);
		this.webApplicationContext = wac;
		if (wac != null) {
			this.setEnvironment(wac.getEnvironment());
		}
	}
    
    
    // doFilter 在這裡,來吧!!! 在這裡,來吧!!! 在這裡,來吧!!!
    @Override
	public void doFilter(ServletRequest request,FilterChain filterChain)
			throws ServletException,IOException {

		// Lazily initialize the delegate if necessary.
		Filter delegateToUse = this.delegate;
		if (delegateToUse == null) {
			synchronized (this.delegateMonitor) {
				delegateToUse = this.delegate;
				if (delegateToUse == null) {
					WebApplicationContext wac = findWebApplicationContext();
					delegateToUse = initDelegate(wac);
				}
				this.delegate = delegateToUse;
			}
		}

		// Let the delegate perform the actual doFilter operation.
        // 直接去這個方法看吧
		invokeDelegate(delegateToUse,filterChain);
	}
    
    // 啥也不幹,直接就拋給了ShiroFilterFactoryBean的SpringShiroFilter這個內部類
	protected void invokeDelegate(
			Filter delegate,ServletRequest request,IOException {

		delegate.doFilter(request,filterChain);
	}
    
}
複製程式碼

SpringShiroFilter

ShiroFilterFactoryBean$SpringShiroFilter的父類OncePerRequestFilter.java(以下便是SpringShiroFilter使命)

public abstract class OncePerRequestFilter extends NameableFilter {
    // 跳到這裡來了
    public final void doFilter(ServletRequest request,FilterChain filterChain) {
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            filterChain.doFilter(request,response);
        } else {
                
            request.setAttribute(alreadyFilteredAttributeName,Boolean.TRUE);

            try {
                // 直接看看覆蓋了這個方法的類吧
                doFilterInternal(request,filterChain);
            } finally {
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }
}
複製程式碼

SpringShiroFilter的父類AbstractShiroFilter.java

public abstract class AbstractShiroFilter extends OncePerRequestFilter {
    // 此處是真正的實現
    protected void doFilterInternal(ServletRequest servletRequest,ServletResponse servletResponse,final FilterChain chain)throws ServletException,IOException {

        try {
            final ServletRequest request = prepareServletRequest(servletRequest,servletResponse,chain);
            final ServletResponse response = prepareServletResponse(request,chain);
			// 在此處建立了 subject喔,記住了喔!!!
            final Subject subject = createSubject(request,response);

            //noinspection unchecked
            // subject建立好,並將下面兩個方法封裝在Callable()裡面再執行,
            // 在執行這個call之前,先將subject繫結到當前執行緒,執行完後,清理當前執行緒的繫結
            // 為什麼非要搞個Callable,直接在 這兩個方法前後放兩個方法就好了,可能是因為這樣擴充套件性更強
            // 以後在外面再封裝一層也方便,說不定還可以搞非同步???
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    // 更新session時間
                    updateSessionLastAccessTime(request,response);
                    //************************************************
                    // 執行shiro過濾鏈,(先講subject建立過程吧,後續再講)
                    //************************************************
                    executeChain(request,chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        }
    }
	//*****************************************
	// 暫時記這個建立過程為subject建立過程
	//*****************************************
    // subjcet 建立過程交給了父類Subject$Builder了,並送了他一個securityManager
	protected WebSubject createSubject(ServletRequest request,ServletResponse response){
        return new WebSubject.Builder(getSecurityManager(),response).buildWebSubject();
    }
    
    
}
//==============================================================>

// 簡單展示,具體自己debug看看
public class SubjectCallable<V> implements Callable<V> {
    public V call() throws Exception {
    	try {
            // 繫結到當前執行緒
        	threadState.bind();
            // 執行自己實現的那個call方法
        	return doCall(this.callable);
    	} finally {
            // 清除資料
        	threadState.restore();
    	}
	}
}

複製程式碼

subject建立過程

Subject$BuilderSubject內部靜態類Build

public interface Subject {
    public static class Builder {
        // 從這兒可以看出,最終委託給了SecurityManager來幹這個
        public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
        }
    }
}
複製程式碼

DefaultSecurityManager.java(以下討論的都本類和本類的父類體系內喔)

public class DefaultSecurityManager extends SessionsSecurityManager {
    // 在subjcetContext基礎上重新new一個喔,不影響前面的subjectContext
	public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        // 就是一個Map,裡面儲存了很多驗證過程的東西,例如是否認證,是否是remember等等,
        // 在DefaultSubjectContext類裡面可以看到,有:securityManager,sessionId,authenticationToken,authenticationInfo,principals,session,authenticated,host,sessionCreationEnabled,principalsSessionKey,authenticatedSessionKey
        // 就是一個臨時狀態和工具集合地,如果是DefaultWebSecurityManager就建立一個
        // DefaultWebSubjectContext例項,實際上本文討論的就是web
        SubjectContext context = copy(subjectContext);

        //確保SecurityManager 已經放到context裡面去了
        context = ensureSecurityManager(context);
        
		// 這個最關鍵,也是最複雜一個,也不復雜,就深入了幾個類,這也是下面要講的重點
        // step0.........
        context = resolveSession(context);
		
        // 這個嘛,等會兒說
        context = resolvePrincipals(context);

        // 前戲已經準備好了,改開始建立Subject了
        Subject subject = doCreateSubject(context);

        // 這個會把當前Subject中最新的資訊同步到session裡面,還有其他功能,後續可以深入看看
        save(subject);

        return subject;
    }
    
    // step1,去解決session
	protected SubjectContext resolveSession(SubjectContext context) {
        if (context.resolveSession() != null) {
            return context;
        }
        try {
            // 直接看resolveContextSession
            Session session = resolveContextSession(context);
            if (session != null) {
                context.setSession(session);
            }
        } catch (InvalidSessionException e) {
        }
        return context;
    }
    // step2,開始了
    protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
        // 先看看context裡面有沒有,有的話就不用繼續找了,省時間,沒有的話,就將request和response包裝到SessionKey裡面
        SessionKey key = getSessionKey(context);
        if (key != null) {
            // 現在真正開始了,但這事得分工,直接交給專門管理session的父類		SessionsSecurityManager(程式碼裡面的啃老族,把活全交給父類幹,很正常喔)
            return getSession(key);
        }
        return null;
    }
}
複製程式碼

SessionsSecurityManager.java(所有session有關操作,由他管理)

public abstract class SessionsSecurityManager extends AuthorizingSecurityManager {
    // step3. 看到這裡,發現SecurityManager整體是不會幹活的,就管著整個流程,然後分發出去
    public Session getSession(SessionKey key) throws SessionException {
        // 這個sessionManger,如果你不覆蓋它,預設就是DefaultSessionManager這個類,我們就從這個開始吧
        return this.sessionManager.getSession(key);
    }
}
複製程式碼

AbstractNativeSessionManager.javaDefaultSessionManager父類)

public abstract class AbstractNativeSessionManager extends AbstractSessionManager implements NativeSessionManager,EventBusAware {
    // step4. 別急,漸漸開始了
    public Session getSession(SessionKey key) throws SessionException {
        Session session = lookupSession(key);
        return session != null ? createExposedSession(session,key) : null;
    }
    
    // step5. 來了
    private Session lookupSession(SessionKey key) throws SessionException {
        if (key == null) {
            throw new NullPointerException("SessionKey argument cannot be null.");
        }
        // 看到do開頭方法,就知道,開始真正幹活了
        return doGetSession(key);
    }
    
}
複製程式碼

AbstractValidatingSessionManager.javaDefaultSessionManager父類)

public abstract class AbstractValidatingSessionManager extends AbstractNativeSessionManager        implements ValidatingSessionManager,Destroyable {
    
    // step6. 開始了
    @Override
    protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
        // 驗證session的有效性,一般不啟動這個分方法(感覺redis可以設定時間控制有效性,可以不啟動驗證)
        enableSessionValidationIfNecessary();
        // 直接看這個吧,這個直接跳到子類DefaultSessionManager中去了,go
        Session s = retrieveSession(key);
        if (s != null) {
            validate(s,key);
        }
        return s;
    }
    
}
複製程式碼

DefaultSessionManager.java

public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {
   	// step7. 繼續
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        // 先取sessionId
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            return null;
        }
        // 通過sessionId來取session
        Session s = retrieveSessionFromDataSource(sessionId);
        if (s == null) {
            throw new UnknownSessionException(msg);
        }
        return s;
    }
    
    // step8. 開始找sessionId
    @Override
    public Serializable getSessionId(SessionKey key) {
        // 檢視key中是否就儲存了這個sessionId(shiro到處搞引用快取,真幾把繞)
        Serializable id = super.getSessionId(key);
        if (id == null && WebUtils.isWeb(key)) {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            // 繼續從這兒開始
            id = getSessionId(request,response);
        }
        return id;
    }
    // 到這裡了,繼續
    protected Serializable getSessionId(ServletRequest request,ServletResponse response) {
        return getReferencedSessionId(request,response);
    }
    // step9. 這裡就找完就結束了,沒找到就沒找到了,開始回到step7,假定已經找到了,ok,繼續
    private Serializable getReferencedSessionId(ServletRequest request,ServletResponse response) {
		// 這個直接從cookie裡面讀,這個過程建議自己debug進去看看,我感覺挺重要的,也很簡單,我就不寫了
        String id = getSessionIdCookieValue(request,response);
        if (id != null) {
            // 儲存到request裡面去
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        } else {

            // 如果沒找到就從請求引數裡面找,這個請求規則是這樣的:http:localhost:8001?;ShiroHttpSession.DEFAULT_SESSION_ID_NAME=sessionId(熟稱shiro小尾巴,真心不好看)
            //try the URI path segment parameters first:
            id = getUriPathSegmentParamValue(request,ShiroHttpSession.DEFAULT_SESSION_ID_NAME);

            if (id == null) {
                //not a URI path segment parameter,try the query parameters:
                String name = getSessionIdName();
                id = request.getParameter(name);
                if (id == null) {
                    //try lowercase:
                    // 還沒找到,那就從request請求引數裡面去找(所以就算瀏覽器存不了cookie,那隻能儲存到請求引數裡面了)
                    id = request.getParameter(name.toLowerCase());
                }
            }
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            }
        }
        // 把剛獲取到的結果都放在request裡面快取起來
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
        }

        // always set rewrite flag - SHIRO-361
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED,isSessionIdUrlRewritingEnabled());

        return id;
    }
    
    // step10. 從你自己設定的儲存來獲取session,如果是redis,就從redis裡面獲取,就到這個了,剩下的自己看吧
    protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
        return sessionDAO.readSession(sessionId);
    }
}
複製程式碼

到這一步,resolveSession(context)這個方法已經完成,只剩下doCreateSubject(context)save(subject)

public class DefaultSecurityManager extends SessionsSecurityManager {

    public Subject createSubject(SubjectContext subjectContext) {
        SubjectContext context = copy(subjectContext);
        context = ensureSecurityManager(context);
        context = resolveSession(context);
        context = resolvePrincipals(context);
		// 來這個很簡單
        Subject subject = doCreateSubject(context);

        save(subject);

        return subject;
    }
    
    // 從方法就看出來,最終使用專門的subjectFactory來建立Subject,本文都在講web
    // 所以預設是 DefaultWebSubjectFactory這個工廠方法
    protected Subject doCreateSubject(SubjectContext context) {
        return getSubjectFactory().createSubject(context);
    }
    
}
複製程式碼

DefaultWebSubjectFactory.java

public class DefaultWebSubjectFactory extends DefaultSubjectFactory {
    
    public Subject createSubject(SubjectContext context) {

        boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
        if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) {
            return super.createSubject(context);
        }
        // 從context這個儲存裡面取值了
        WebSubjectContext wsc = (WebSubjectContext) context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        Session session = wsc.resolveSession();
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        PrincipalCollection principals = wsc.resolvePrincipals(); // 
        boolean authenticated = wsc.resolveAuthenticated(); // 是否認證了
        String host = wsc.resolveHost();
		// 還有request和response,是不是subject存了很多,但你卻基本上沒用過,沒事別亂搞事喔
        ServletRequest request = wsc.resolveServletRequest();	
        ServletResponse response = wsc.resolveServletResponse();
		// 建立一個真正的Subject
        return new WebDelegatingSubject(principals,sessionEnabled,securityManager);
    }
}
複製程式碼

以上就是Subject建立過程,如果有session就填充進去,沒有就不填充,但是Subject必須創建出來。好現在讓我們回到AbstractShiroFilter這個類,繼續看doFilterInternal這個方法,前戲已經足夠充分了,改執行shiro 過濾鏈了。來,come on ...... 馬上要講完了

AbstractShiroFilter.java (shiro filter開始分發執行了)

public abstract class AbstractShiroFilter extends OncePerRequestFilter {
    protected void doFilterInternal(ServletRequest servletRequest,final FilterChain chain)
            throws ServletException,IOException {

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest,chain);

            // 已經完成
            final Subject subject = createSubject(request,response);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request,response);
                    // 來,come on
                    executeChain(request,chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        }

        if (t != null) {
            throw new ServletException(msg,t);
        }
    }
    
    // 在這裡
    protected void executeChain(ServletRequest request,FilterChain origChain)
            throws IOException,ServletException {
        // 這個作用就是根據請求的URL,
        // 從 Map<String,String>這個對映中找出第一個匹配的value,以及對應的filtes,對映長這麼樣
        // filterChainDefinitionMap.put("/login","casFilter");
        // filterChainDefinitionMap.put("/favicon.ico","anon");
        // filterChainDefinitionMap.put("/**/*.html","anon");
        // filterChainDefinitionMap.put("/**","authc,anon"); 這就有2個喔,也就是chain中有兩個filter
        FilterChain chain = getExecutionChain(request,origChain);
        // 這個chain就是ProxiedFilterChain(web下就是這個喔),走,去這個類看看,功能很簡單
        chain.doFilter(request,response);
    }
}
複製程式碼

ProxiedFilterChain.java (多個filter時,執行策略)

public class ProxiedFilterChain implements FilterChain {
    // 執行過濾鏈策略,其實就是把當前chain,當成所有filter的chain,使用本地index變數來確定下一個要執行的filter
	public void doFilter(ServletRequest request,ServletResponse response) throws IOException,ServletException {
        if (this.filters == null || this.filters.size() == this.index) {

            this.orig.doFilter(request,response);
        } else {

            this.filters.get(this.index++).doFilter(request,this);
        }
    }
}
複製程式碼

到現在shiro已經講了一大半了,還剩下實際執行一個filter過程,我就拿casFilter來講吧。

shiro cas 請求時序圖

從圖中可以看出來整個認證過程,我就直接將casFilter.java,這個是使用者拿到了那個token,然後向伺服器發起了請求,現在CasFilter.java的doFilter攔截到他了,來是時候做個了斷了(下面用的Filter抽象類,很熟悉吧,前面講過喔):

OncePerRequestFilter.java(CasFilter的父類)

public abstract class OncePerRequestFilter extends NameableFilter {
    
    public final void doFilter(ServletRequest request,FilterChain filterChain){
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            filterChain.doFilter(request,response);
        } else
            if (!isEnabled(request,response) || shouldNotFilter(request) ) {

            filterChain.doFilter(request,response);
        } else {
            request.setAttribute(alreadyFilteredAttributeName,Boolean.TRUE);
            try {
                // 直接看這個吧,我直接跳到CasFilter的onAccessDenied方法吧,
                // why => 前面這塊講得真的很清楚喔
                // 不知道的請跳到前面講 shiro filter那塊
                doFilterInternal(request,filterChain);
            } finally {
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }
}
複製程式碼

CasFilter.java

public class CasFilter extends AuthenticatingFilter {
    // 前面那些步驟啥也沒幹,直到這裡開始幹活了
    protected boolean onAccessDenied(ServletRequest request,ServletResponse response) throws Exception {
        // 這個方法是父類AuthenticatingFilter的,走去這裡
        return executeLogin(request,response);
    }
}
複製程式碼

AuthenticatingFilter.java

public abstract class AuthenticatingFilter extends AuthenticationFilter {
    
    protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception {
        // 取出請求引數裡面的token,幷包裝成casToken
        AuthenticationToken token = createToken(request,response);
        try {
            // 看到這裡清楚了吧,使用SecurityManager提供的介面,開始驗證了喔
            Subject subject = getSubject(request,response);
            // 登入,走起
            subject.login(token);
            return onLoginSuccess(token,response);
        }
    }
}
複製程式碼

DelegatingSubject.java

public class DelegatingSubject implements Subject {

    public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        // 直接看這個吧,登入操作肯定交給securityManager了
        // step1. 開始的地方
        Subject subject = securityManager.login(this,token);

        PrincipalCollection principals;

        String host = null;

        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals == null || principals.isEmpty()) {
            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;
        }
    }
    
}
複製程式碼

DefaultSecurityManager.java

public class DefaultSecurityManager extends SessionsSecurityManager {

    public Subject login(Subject subject,AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            // step2. 為託給了父類 AuthenticatingSecurityManager
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                // 失敗後,估計就跳轉登入了
                onFailedLogin(token,ae,subject);
            } catch (Exception e) {
            }
            throw ae; //propagate
        }
		// 認證成功,重新封裝subject
        Subject loggedIn = createSubject(token,info,subject);
		
        // 這個跟rememberMe有關
        onSuccessfulLogin(token,loggedIn);

        return loggedIn;
    } 
}


複製程式碼

AuthenticatingSecurityManager.java

public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
	// step3. 繼續委託給Authenticator,如果你沒配置,預設就是ModularRealmAuthenticator
    // 本文還是重新配置了(建議多個realm時必須重新配置這個,
    // 除非你的認證策略跟ModularRealmAuthenticator一樣)
	public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        // 直接進入
        return this.authenticator.authenticate(token);
    }
}
複製程式碼

AbstractAuthenticator.java

public abstract class AbstractAuthenticator implements Authenticator,LogoutAware {
    
    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        }
        AuthenticationInfo info;
        try {
            // step4. do開頭說明真的開始了,
            // 本文是自己實現的Authenticator(MyModularRealmAuthenticator),去這裡
            info = doAuthenticate(token);
            if (info == null) {
                throw new AuthenticationException(msg);
            }
        } catch (Throwable t) {
            AuthenticationException ae = null;
            if (t instanceof AuthenticationException) {
                ae = (AuthenticationException) t;
            }
            if (ae == null) {
                ae = new AuthenticationException(msg,t);
            }
            try {
                notifyFailure(token,ae);
            } catch (Throwable t2) {
            }


            throw ae;
        }

        notifySuccess(token,info);

        return info;
    }
}
複製程式碼

MyModularRealmAuthenticator.java

public class MyModularRealmAuthenticator extends ModularRealmAuthenticator { 
    
    // 當有多個realm時,應該如何使用,本文策略就是:如果是castoken就讓他走casRealm,其他的走單個認真方式
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 所有Realm
        Collection<Realm> realms = getRealms();
        HashMap<String,Realm> realmHashMap = new HashMap<>(realms.size());
        for (Realm realm : realms) {
            realmHashMap.put(realm.getName(),realm);
        }
        
        if (authenticationToken instanceof CasToken) {
            // step5. 直接進入這個方法吧
            return doSingleRealmAuthentication(realmHashMap.get("casRealm"),authenticationToken);
        } else {
            return doSingleRealmAuthentication(realmHashMap.get("tokenRealm"),authenticationToken);
        }
    }
}
複製程式碼

ModularRealmAuthenticator.java

public class ModularRealmAuthenticator extends AbstractAuthenticator {
    
    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm,AuthenticationToken token) {
        if (!realm.supports(token)) {
            throw new UnsupportedTokenException(msg);
        }
         // step6. 直接進入相應realm了,本文是CasRealm,走去casrealm看看
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            throw new UnknownAccountException(msg);
        }
        return info;
    }  
}
複製程式碼

AuthenticatingRealm.javaCasRealm父類)

public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
    
    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// 先檢視這個realm有沒有配置快取,有的話直接從快取裡面取
        // 如果你配置CacheManager,casRealm1.setAuthorizationCachingEnabled(true),則會使用快取喔,這個在使用者名稱密碼登入,在這裡加一個快取,可以加快認證速度,cas則不需要(不是不需要,是不能用)
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached,perform the lookup:
            // step7. 來這裡吧,一般自己寫個realm,就覆蓋do開頭的方法(因為覆蓋就是為了幹活喔)
            info = doGetAuthenticationInfo(token);
            if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token,info);
            }
        }

        if (info != null) {
            assertCredentialsMatch(token,info);
        } 

        return info;
    }
}
複製程式碼

CasRealm.java

public class CasRealm extends AuthorizingRealm {

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        CasToken casToken = (CasToken) token;
        if (token == null) {
            return null;
        }
        
        String ticket = (String)casToken.getCredentials();
        if (!StringUtils.hasText(ticket)) {
            return null;
        }
        // 預設使用 Cas20ServiceTicketValidator 來進行通訊,跟前端調後端介面一樣
        TicketValidator ticketValidator = ensureTicketValidator();

        try {
            // step8. 來這裡吧,要開始跟cas伺服器通訊了,驗證下token的正確性
            // 這個過程就不說了,建議自己debug進去看看,是怎麼通訊的,我在專案裡寫了這個模擬,可以看看
            Assertion casAssertion = ticketValidator.validate(ticket,getCasService());
            
            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
            String userId = casPrincipal.getName();


            Map<String,Object> attributes = casPrincipal.getAttributes();
            // refresh authentication token (user id + remember me)
            casToken.setUserId(userId);
            String rememberMeAttributeName = getRememberMeAttributeName();
            String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
            boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
            if (isRemembered) {
                casToken.setRememberMe(true);
            }

            List<Object> principals = CollectionUtils.asList(userId,attributes);
            PrincipalCollection principalCollection = new SimplePrincipalCollection(principals,getName());
            
            // 上面不多說了設定
            return new SimpleAuthenticationInfo(principalCollection,ticket);
        } catch (TicketValidationException e) { 
            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]",e);
        }
    }
    // doGetAuthorizationInfo 這個方法必須注意,後去驗證角色,角色許可權,都會呼叫到這個方法,
    // 因此請務必重寫,注意點為:1為角色roles,2角色的許可權permission
    // PS: 你可以把AuthorizationInfo封裝後放在session裡,這樣每次呼叫這個方法就從session裡面取
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // retrieve user information
        SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
        List<Object> listPrincipals = principalCollection.asList();
        Map<String,String> attributes = (Map<String,String>) listPrincipals.get(1);
        // create simple authorization info
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // add default roles
        addRoles(simpleAuthorizationInfo,split(defaultRoles));
        // add default permissions
        addPermissions(simpleAuthorizationInfo,split(defaultPermissions));
        // get roles from attributes
        List<String> attributeNames = split(roleAttributeNames);
        for (String attributeName : attributeNames) {
            String value = attributes.get(attributeName);
            addRoles(simpleAuthorizationInfo,split(value));
        }
        // get permissions from attributes
        attributeNames = split(permissionAttributeNames);
        for (String attributeName : attributeNames) {
            String value = attributes.get(attributeName);
            addPermissions(simpleAuthorizationInfo,split(value));
        }
        return simpleAuthorizationInfo;
    }
}
複製程式碼

到這裡差不多就結束了,具體使用可以參見網上使用方法,你也可以在程式碼裡使用註解,shiro有自己的aop實現,他會把那些打註解的類,方法進行代理。

​ 最後說下可以優化和注意的點:

  1. WebSessionManager在每次獲取session的時候都會從SessionDAO裡面讀取,如果快取是redis,這樣很消耗效能,最好重寫retrieveSession這個方法,將第一次獲取到的Session存放到request裡面去,後面每次從這裡面取。
  2. 就是使用RedisSession序列化儲存的時候,SimpleSession裡面欄位都是transient修飾的,選擇序列化方案時,請注意。要麼自己重寫SimpleSession,要麼選一個不會忽略transient的序列化方式。
  3. 那些不需要認證的資源跟需要認證的資源一樣都會從 SessionDAO獲取一次Session,其實這個完全沒必要,可以,這個也可以在WebSessionManager裡面進行優化。
  4. 不管什麼請求傳送到伺服器,伺服器都會先把請求生成一個會話儲存到 會話儲存的地方,如果有人一直請求,會造成 會話儲存跑滿,最終造成拒絕服務攻擊。(解決辦法,將沒有認證的會話和認證過的會話放在不同的地方,也可以不儲存沒有認真的會話,但不儲存會導致使用者第一次登入認證,不會導航到使用者第一次訪問的那個地址,而使原先設定好的地址,這會影響使用者體驗)

License

Apache License,Version 2.0