Shiro原始碼分析----認證流程
阿新 • • 發佈:2019-01-04
由於本文是基於原始碼分析Shiro認證流程,所以假設閱讀者對Shiro已經有一定的瞭解,如果對Shiro還不大瞭解的話,推薦一下博文:跟我學Shiro目錄貼
Apache Shiro作為一個優秀的許可權框架,其最重要的兩項工作:其一是認證,即解決登入的使用者的身份是否合法;其二是使用者登入後有什麼樣的許可權。本文將基於Shiro原始碼來剖析Shiro的認證流程,只有深層次的理解Shiro認證流程,認證過程中各個元件的作用,才能在實際應用中靈活使用。由於Shiro一般用於Web環境且會與Spring整合使用,所以此次認證流程的分析的前提也是Web環境且Shiro已與Spring整合。
特別說明:本文使用的Shiro版本:1.2.2。
熟悉Spring的人應該都知道DelegatingFilterProxy的作用,該Spring提供的過濾器只起委託作用,執行流程委託給Spring容器中名為shiroFilter的過濾器。所以還需要在Spring配置檔案中配置shiroFilter,如下:
ShiroFilterFactoryBean實現了org.springframework.beans.factory.FactoryBean介面,所以shiroFilter物件是由ShiroFilterFactoryBean的getObject()方法返回的:
從上述原始碼中可以看到,最終返回了一個SpringShiroFilter物件,即Spring配置檔案中的shiroFilter物件,該過濾器擁有三個重要物件:SecurityManager、PathMatchingFilterChainResolver、FilterChainManager。
由於在Spring配置中設定了filterChainDefinitions屬性,所以會呼叫setFilterChainDefinitions方法:
FilterChainManager用於管理當前Shiro應用的所有Filter,有Shiro預設使用的Filter,也可以是自定義的Filter。下面我們看看FilterChainManager是如何創建出來的:
PathMatchingFilterChainResolver物件職責很簡單,就是使用ant路徑匹配方法匹配訪問的URL,由於pathMatchingFilterChainResolver擁有FilterChainManager物件,所以URL匹配上後可以獲取該URL需要應用的FilterChain了。
通過上述分析可以知道,Shiro就是通過一系列的URL匹配符配置URL應該應用上的Filter,然後在Filter中完成相應的任務,所以Shiro的所有功能都是通過Filter完成的。當然認證功能也不例外,在上述配置中認證功能是由org.apache.shiro.web.filter.authc.FormAuthenticationFilter完成的。
下面我們就看看入口過濾器SpringShiroFilter的執行流程,是如何執行到FormAuthenticationFilter的。既然是Filter,那麼最重要的就是doFilter方法了,由於SpringShiroFilter繼承自OncePerRequestFilter,doFilter方法也是在OncePerRequestFilter中定義的:
doFilterInternal方法定義AbstractShiroFilter中:
先看一下,Subject如果是如何建立的:
跟蹤程式碼最終呼叫DefaultWebSubjectFactory.createSubject方法:
接下來看看過濾器鏈是如何建立與執行的:
根據上述Spring配置,假設現在第一次訪問URL: "/authenticated.jsp",則會應用上名為authc的Filter,即FormAuthenticationFilter,根據FormAuthenticationFilter的繼承體系,先執行dviceFilter.doFilterInternal方法:
接下來執行:PathMatchingFilter.preHandle方法:
接著執行AccessControlFilter.onPreHandle方法:
接著執行AuthenticatingFilter.isAccessAllowed方法:
由以上程式碼可知,由於是第一次訪問URL:"/authenticated.jsp",所以isAccessAllowed方法返回false,所以接著執行FormAuthenticationFilter.onAccessDenied方法:
根據配置,訪問URL:"/login.jsp"時也會應用上FormAuthenticationFilter,由於是重定向所以發起的是GET請求,所以isLoginSubmission()返回false,所以沒有執行executeLogin方法,所以能夠訪問/login.jsp頁面。在登入表單中應該設定action="",這樣登入請求會提交至/login.jsp,這時為POST請求,所以會執行executeLogin方法:
至此,認證流程大致流程就是這樣了,限於篇幅,登入的流程具體,請期待下篇博文。
Apache Shiro作為一個優秀的許可權框架,其最重要的兩項工作:其一是認證,即解決登入的使用者的身份是否合法;其二是使用者登入後有什麼樣的許可權。本文將基於Shiro原始碼來剖析Shiro的認證流程,只有深層次的理解Shiro認證流程,認證過程中各個元件的作用,才能在實際應用中靈活使用。由於Shiro一般用於Web環境且會與Spring整合使用,所以此次認證流程的分析的前提也是Web環境且Shiro已與Spring整合。
特別說明:本文使用的Shiro版本:1.2.2。
Shiro與Spring整合時,需要在web.xml中配置Shiro入口過濾器:
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
熟悉Spring的人應該都知道DelegatingFilterProxy的作用,該Spring提供的過濾器只起委託作用,執行流程委託給Spring容器中名為shiroFilter的過濾器。所以還需要在Spring配置檔案中配置shiroFilter,如下:
<!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /index.jsp = anon /unauthorized.jsp = anon /login.jsp = authc /logout = logout /authenticated.jsp = authc /** = user </value> </property> </bean>
ShiroFilterFactoryBean實現了org.springframework.beans.factory.FactoryBean介面,所以shiroFilter物件是由ShiroFilterFactoryBean的getObject()方法返回的:
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
// 獲取配置檔案中設定的安全管理器
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
// 必須是Web環境的安全管理器
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
// 建立過濾器鏈管理器
FilterChainManager manager = createFilterChainManager();
// 建立基於路徑匹配的過濾器鏈解析器
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
// 返回SpringShiroFilter物件
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
從上述原始碼中可以看到,最終返回了一個SpringShiroFilter物件,即Spring配置檔案中的shiroFilter物件,該過濾器擁有三個重要物件:SecurityManager、PathMatchingFilterChainResolver、FilterChainManager。
由於在Spring配置中設定了filterChainDefinitions屬性,所以會呼叫setFilterChainDefinitions方法:
public void setFilterChainDefinitions(String definitions) {
Ini ini = new Ini();
ini.load(definitions);
//did they explicitly state a 'urls' section? Not necessary, but just in case:
Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
if (CollectionUtils.isEmpty(section)) {
//no urls section. Since this _is_ a urls chain definition property, just assume the
//default section contains only the definitions:
section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
}
/** 獲取預設section,也就是載入
/index.jsp = anon
/unauthorized.jsp = anon
/login.jsp = authc
/logout = logout
/authenticated.jsp = authc
/** = user
這段配置,從這段配置中可以知道哪種URL需要應用上哪些Filter,像anon、authc、logout就是Filter的名稱,
Ini.Section實現了Map介面,其key為URL匹配符,value為Filter名稱
**/
// 設定filterChainDefinitionMap
setFilterChainDefinitionMap(section);
}
FilterChainManager用於管理當前Shiro應用的所有Filter,有Shiro預設使用的Filter,也可以是自定義的Filter。下面我們看看FilterChainManager是如何創建出來的:
protected FilterChainManager createFilterChainManager() {
// 建立DefaultFilterChainManager
DefaultFilterChainManager manager = new DefaultFilterChainManager();
// 建立Shiro預設Filter,根據org.apache.shiro.web.filter.mgt.DefaultFilter建立
Map<String, Filter> defaultFilters = manager.getFilters();
//apply global settings if necessary:
for (Filter filter : defaultFilters.values()) {
// 設定相關Filter的loginUrl、successUrl、unauthorizedUrl屬性
applyGlobalPropertiesIfNecessary(filter);
}
// 獲取在Spring配置檔案中配置的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);
}
// 將配置的Filter新增至鏈中,如果同名Filter已存在則覆蓋預設Filter
manager.addFilter(name, filter, false);
}
}
//build up the chains:
Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
// 為配置的每一個URL匹配建立FilterChain定義,
// 這樣當訪問一個URL的時候,一旦該URL配置上則就知道該URL需要應用上哪些Filter
// 由於URL配置符會配置多個,所以以第一個匹配上的為準,所以越具體的匹配符應該配置在前面,越寬泛的匹配符配置在後面
manager.createChain(url, chainDefinition);
}
}
return manager;
}
PathMatchingFilterChainResolver物件職責很簡單,就是使用ant路徑匹配方法匹配訪問的URL,由於pathMatchingFilterChainResolver擁有FilterChainManager物件,所以URL匹配上後可以獲取該URL需要應用的FilterChain了。
通過上述分析可以知道,Shiro就是通過一系列的URL匹配符配置URL應該應用上的Filter,然後在Filter中完成相應的任務,所以Shiro的所有功能都是通過Filter完成的。當然認證功能也不例外,在上述配置中認證功能是由org.apache.shiro.web.filter.authc.FormAuthenticationFilter完成的。
下面我們就看看入口過濾器SpringShiroFilter的執行流程,是如何執行到FormAuthenticationFilter的。既然是Filter,那麼最重要的就是doFilter方法了,由於SpringShiroFilter繼承自OncePerRequestFilter,doFilter方法也是在OncePerRequestFilter中定義的:
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 用於保證鏈中同一型別的Filter只會被執行一次
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 執行真正的功能程式碼
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
doFilterInternal方法定義AbstractShiroFilter中:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
// 建立Subject物件,由此可見,每一個請求到來,都會呼叫createSubject方法
final Subject subject = createSubject(request, response);
// 通過Subject物件執行過濾器鏈,
subject.execute(new Callable() {
public Object call() throws Exception {
// 更新會話最後訪問時間,用於計算會話超時
updateSessionLastAccessTime(request, response);
// 執行過濾器鏈
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
// 省略一些程式碼...
}
先看一下,Subject如果是如何建立的:
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
跟蹤程式碼最終呼叫DefaultWebSubjectFactory.createSubject方法:
public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
}
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
// 判斷是已經認證,如果是在沒有登入之前,明顯返回是false
boolean authenticated = wsc.resolveAuthenticated();
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
request, response, securityManager);
}
接下來看看過濾器鏈是如何建立與執行的:
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
// 獲取當前URL匹配的過濾器鏈
FilterChain chain = getExecutionChain(request, response, origChain);
// 執行過濾器鏈中的過濾器
chain.doFilter(request, response);
}
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
// 獲取過濾器鏈解析器,即上面建立的PathMatchingFilterChainResolver物件
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}
// 呼叫其getChain方法,根據URL匹配相應的過濾器鏈
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
log.trace("Resolved a configured FilterChain for the current request.");
chain = resolved;
} else {
log.trace("No FilterChain configured for the current request. Using the default.");
}
return chain;
}
根據上述Spring配置,假設現在第一次訪問URL: "/authenticated.jsp",則會應用上名為authc的Filter,即FormAuthenticationFilter,根據FormAuthenticationFilter的繼承體系,先執行dviceFilter.doFilterInternal方法:
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
Exception exception = null;
try {
// 執行preHandle
boolean continueChain = preHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");
}
// 如果preHandle返回false則過濾器鏈不再執行
if (continueChain) {
executeChain(request, response, chain);
}
postHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Successfully invoked postHandle method");
}
} catch (Exception e) {
exception = e;
} finally {
cleanup(request, response, exception);
}
}
接下來執行:PathMatchingFilter.preHandle方法:
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately.");
}
return true;
}
for (String path : this.appliedPaths.keySet()) {
// 根據配置,訪問URL:"/authenticated.jsp"時,會匹配上FormAuthenticationFilter,
// 而FormAuthenticationFilter繼承自PathMatchingFilter,所以返回true
if (pathsMatch(path, request)) {
log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path);
Object config = this.appliedPaths.get(path);
// 執行isFilterChainContinued方法,該方法呼叫onPreHandle方法
return isFilterChainContinued(request, response, path, config);
}
}
//no path matched, allow the request to go through:
return true;
}
接著執行AccessControlFilter.onPreHandle方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
// 如果isAccessAllowed方法返回false,則會執行onAccessDenied方法
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
接著執行AuthenticatingFilter.isAccessAllowed方法:
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
super.isAccessAllowed方法,即AuthenticationFilter.isAccessAllowed方法:
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
由以上程式碼可知,由於是第一次訪問URL:"/authenticated.jsp",所以isAccessAllowed方法返回false,所以接著執行FormAuthenticationFilter.onAccessDenied方法:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 第一次訪問自然不是登入請求
if (isLoginRequest(request, response)) {
// 判斷是否是POST請求
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
// 所以執行該方法
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
// 將request物件儲存在session中,以便登入成功後重新轉至上次訪問的URL
saveRequest(request);
// 重定向至登入頁面,即:"/login.jsp"
redirectToLogin(request, response);
}
根據配置,訪問URL:"/login.jsp"時也會應用上FormAuthenticationFilter,由於是重定向所以發起的是GET請求,所以isLoginSubmission()返回false,所以沒有執行executeLogin方法,所以能夠訪問/login.jsp頁面。在登入表單中應該設定action="",這樣登入請求會提交至/login.jsp,這時為POST請求,所以會執行executeLogin方法:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 根據表單填寫的使用者名稱密碼建立AuthenticationToken
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
// 獲取Subject物件
Subject subject = getSubject(request, response);
// 執行Subject.login方法進行登入
subject.login(token);
// 如果登入成功,重定向至上次訪問的URL
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
// 如果登入失敗,則設定錯誤資訊至request,並重新返回登入頁面
return onLoginFailure(token, e, request, response);
}
}
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request, ServletResponse response) throws Exception {
// 重定向至上次訪問的URL
issueSuccessRedirect(request, response);
// 由於返回false,所以過濾器鏈不再執行
return false;
}
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
ServletRequest request, ServletResponse response) {
// 設定錯誤資訊至request
setFailureAttribute(request, e);
// 由於返回true,所以過濾器鏈繼續執行,所以又返回了登入頁面
return true;
}
至此,認證流程大致流程就是這樣了,限於篇幅,登入的流程具體,請期待下篇博文。