shiro源碼分析(一)--大致流程

去GitHub搜索shiro,克隆源碼,然後會看到裏面有samples模塊,先從quickstart看起

一、例子源碼

首先是配置文件shiro.ini

[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

ini配置中主要配置有四大類:main,users,roles,urls,這裏只包含了users,roles。
[main]
#提供了對根對象 securityManager 及其依賴的配置
securityManager=org.apache.shiro.mgt.DefaultSecurityManager
securityManager.realms=$jdbcRealm

[users]
#提供了對用戶/密碼及其角色的配置,用戶名=密碼,角色 1,角色 2
username=password,role1,role2

[roles]
#提供了角色及權限之間關係的配置,角色=權限 1,權限 2
role1=permission1,permission2

[urls]
#用於 web,提供了對 web url 攔截相關的配置,url=攔截器[參數],攔截器
/index.html = anon
/admin/** = authc, roles[admin], perms[“permission1”]

java類主方法

	public static void main(String[] args) {
		//1、工廠解析配置文件
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
		//2、獲取安全管理器
        SecurityManager securityManager = factory.getInstance();
		//設置安全管理器
		SecurityUtils.setSecurityManager(securityManager);
		//3、獲取當前用戶
		Subject currentUser = SecurityUtils.getSubject();
		//4、獲取session
		Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
		...
		if (!currentUser.isAuthenticated()) {
			//把登錄的信息封裝到token中
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
			//5、登錄
                currentUser.login(token);
            }catch...
		}
	
		...
		6、登出
        currentUser.logout();
	}

二、跟蹤源碼分析

1、解析配置文件new IniSecurityManagerFactory(“classpath:shiro.ini”)

	public IniSecurityManagerFactory(String iniResourcePath) {
        this(Ini.fromResourcePath(iniResourcePath));
    }
	public IniSecurityManagerFactory(Ini config) {
        this();
        setIni(config);
    }

解析配置文件是用了Ini類的fromResourcePath方法。

	public static Ini fromResourcePath(String resourcePath) throws ConfigurationException {
        ...
        Ini ini = new Ini();
        ini.loadFromPath(resourcePath);
        return ini;
    }
	public void loadFromPath(String resourcePath) throws ConfigurationException {
        InputStream is;
        try {
            is = ResourceUtils.getInputStreamForPath(resourcePath);
        } catch...
        load(is);
    }
	public void load(InputStream is) throws ConfigurationException {
        ...
        InputStreamReader isr;
        try {
            isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);
        } catch...
        load(isr);
    }
	public void load(Reader reader) {
        Scanner scanner = new Scanner(reader);
        try {
            load(scanner);
        } finally {
            ...
        }
    }
	public void load(Scanner scanner) {
		//當前解析的配置類別
        String sectionName = DEFAULT_SECTION_NAME;
        StringBuilder sectionContent = new StringBuilder();
        while (scanner.hasNextLine()) {
            String rawLine = scanner.nextLine();
            String line = StringUtils.clean(rawLine);
			//#號和;號開頭的跳過
            if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
                continue;
            }
            String newSectionName = getSectionName(line);
            if (newSectionName != null) {
                //解析到達了新的配置類別,所以要把緩存中的內容先解析
                addSection(sectionName, sectionContent);
                //相當於清空
                sectionContent = new StringBuilder();
                sectionName = newSectionName;//更新當前要解析的配置類別
				...
            } else {
                //添加內容當緩存
                sectionContent.append(rawLine).append("\n");
            }
        }
        //解析緩存的配置
        addSection(sectionName, sectionContent);
    }

解析配置文件是調用了Ini對象的loadFromPath方法,Ini類主要結構如下
public class Ini implements Map<String, Ini.Section> {
//裝每一類配置的內容,如main、users、roles…
private final Map<String, Section> sections;
}
接下來是解析每一類配置

	private void addSection(String name, StringBuilder content) {
        if (content.length() > 0) {
            String contentString = content.toString();
            String cleaned = StringUtils.clean(contentString);
            if (cleaned != null) {
                Section section = new Section(name, contentString);
                if (!section.isEmpty()) {
                    sections.put(name, section);
                }
            }
        }
    }

解析的內容存在Section對象裏,然後放進sections中緩存。Section是Ini的內部類,主要結構如下
public static class Section implements Map<String, String> {
private final String name;//配置類別:如users、roles
private final Map<String, String> props;//如:root --> 123,admin
}

2、獲取安全管理器

	public T getInstance() {
        T instance;
        if (isSingleton()) {//默認是true
            if (this.singletonInstance == null) {
                this.singletonInstance = createInstance();
            }
            instance = this.singletonInstance;
        } else {
            instance = createInstance();
        }
        if (instance == null) {
            throw ...
        }
        return instance;
    }

	public T createInstance() {
		//如果沒提供配置文件,shiro會嘗試加載classpath:shiro.ini文件
        Ini ini = resolveIni();
        T instance;
        if (CollectionUtils.isEmpty(ini)) {
            ...
            instance = createDefaultInstance();
            if (instance == null) {
                throw ...
            }
        } else {
            instance = createInstance(ini);
            if (instance == null) {
                throw ...
            }
        }
        return instance;
    }

最後調用的是IniSecurityManagerFactory的createInstance方法來創建實例

	protected SecurityManager createInstance(Ini ini) {
        ...
        SecurityManager securityManager = createSecurityManager(ini);
        if (securityManager == null) {
            throw ...
        }
        return securityManager;
    }

	private SecurityManager createSecurityManager(Ini ini) {
        return createSecurityManager(ini, getConfigSection(ini));
    }

	private Ini.Section getConfigSection(Ini ini) {
		//獲取[main]的配置
        Ini.Section mainSection = ini.getSection(MAIN_SECTION_NAME);
        if (CollectionUtils.isEmpty(mainSection)) {
            //嘗試獲取“”的配置
            mainSection = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        }
        return mainSection;
    }

在上面解析配置文件的時候可以看到,一開始解析的配置是DEFAULT_SECTION_NAME("“空字符串),所以這裏當[main]不存在的時候會嘗試獲取”"的配置。

接下來開始創建安全管理器

	private SecurityManager createSecurityManager(Ini ini, Ini.Section mainSection) {
		//添加對象緩存到ReflectionBuilder對象
        getReflectionBuilder().setObjects(createDefaults(ini, mainSection));
		//處理[main]配置
        Map<String, ?> objects = buildInstances(mainSection);
		//獲取以SECURITY_MANAGER_NAME爲key的對象
        SecurityManager securityManager = getSecurityManagerBean();
		//這裏根據securityManager裏是否已有Realm對象,沒有就返回true
        boolean autoApplyRealms = isAutoApplyRealms(securityManager);

        if (autoApplyRealms) {
            //從緩存的bean中找出realm
            Collection<Realm> realms = getRealms(objects);
            //設進securityManager裏
            if (!CollectionUtils.isEmpty(realms)) {
                applyRealmsToSecurityManager(realms, securityManager);
            }
        }
        return securityManager;
    }

這裏涉及到ReflectionBuilder類,個人把它理解成是相當於spring中的容器的概念,它主要負責對bean的實例化和管理,裏面還實現了事件訂閱監聽機制,如bean被初始化後會發佈InitializedBeanEvent事件

創建默認的bean如下:

	protected Map<String, ?> createDefaults(Ini ini, Ini.Section mainSection) {
        Map<String, Object> defaults = new LinkedHashMap<String, Object>();
		//創建了一個DefaultSecurityManager
        SecurityManager securityManager = createDefaultInstance();
		//放入了map中,鍵爲SECURITY_MANAGER_NAME
        defaults.put(SECURITY_MANAGER_NAME, securityManager);
		//如果[users]、[roles]配置不爲空,就會創建一個Realm
        if (shouldImplicitlyCreateRealm(ini)) {
            Realm realm = createRealm(ini);
            if (realm != null) {//放入map中
                defaults.put(INI_REALM_NAME, realm);
            }
        }
		...
        return defaults;
    }

可以看到已經默認創建了一個DefaultSecurityManager,所以就算不配置[main]也不會報錯

3、獲取當前用戶 SecurityUtils.getSubject()

	public static Subject getSubject() {
        Subject subject = ThreadContext.getSubject();
        if (subject == null) {//如果爲空就創建
            subject = (new Subject.Builder()).buildSubject();
            ThreadContext.bind(subject);//綁定到線程
        }
        return subject;
    }

>涉及到了ThreadContext類,結構大致如下
>
	public abstract class ThreadContext {
		//用於綁定線程數據
		private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
>
		public static Subject getSubject() {
        	return (Subject) get(SUBJECT_KEY);
    	}
>
		public static Object get(Object key) {
	        ...
	        Object value = getValue(key);
			...
	        return value;
	    }
>
		private static Object getValue(Object key) {
	        Map<Object, Object> perThreadResources = resources.get();
	        return perThreadResources != null ? perThreadResources.get(key) : null;
	    }
>
		public static void bind(Subject subject) {
	        if (subject != null) {
	            put(SUBJECT_KEY, subject);
	        }
	    }
		public static void put(Object key, Object value) {
	        if (key == null) {
	            throw ...
	        }
	        if (value == null) {
	            remove(key);
	            return;
	        }
	        ensureResourcesInitialized();
	        resources.get().put(key, value);
			...
	    }
	}

可以看到主要是用到了ThreadLocal類來綁定線程數據

創建用戶(new Subject.Builder()).buildSubject():

	public Builder() {
        this(SecurityUtils.getSecurityManager());
    }

	public Builder(SecurityManager securityManager) {
        if (securityManager == null) {
            throw ...
        }
        this.securityManager = securityManager;
		//這裏創建一個DefaultSubjectContext對象
        this.subjectContext = newSubjectContextInstance();
        if (this.subjectContext == null) {
            throw ...
        }
        this.subjectContext.setSecurityManager(securityManager);
    }

這裏涉及到SubjectContext接口,SubjectContext裝載着用戶相關的信息,如host、session、token等,DefaultSubjectContext是它的一個實現類。

	public Subject buildSubject() {
        return this.securityManager.createSubject(this.subjectContext);
    }
	
	public Subject createSubject(SubjectContext subjectContext) {
        //新建一個subjectContext,並且複製原來的屬性
        SubjectContext context = copy(subjectContext);
        //保證SubjectContext包含安全管理器
        context = ensureSecurityManager(context);

        //這裏如果context的sessionId不爲空,就會從SessionManager查詢對應sessionId的Session對象,最後將其設進context裏
        context = resolveSession(context);
        context = resolvePrincipals(context);
		//開始創建Subject
        Subject subject = doCreateSubject(context);
		//調用SubjectDAO的save方法
        save(subject);
        return subject;
    }
	protected Subject doCreateSubject(SubjectContext context) {
        return getSubjectFactory().createSubject(context);
    }

	public Subject createSubject(SubjectContext context) {
        SecurityManager securityManager = context.resolveSecurityManager();
        Session session = context.resolveSession();
        boolean sessionCreationEnabled = context.isSessionCreationEnabled();
        PrincipalCollection principals = context.resolvePrincipals();
        boolean authenticated = context.resolveAuthenticated();
        String host = context.resolveHost();
        return new DelegatingSubject(principals, authenticated, host, session, sessionCreationEnabled, securityManager);
    }

最後返回的是DelegatingSubject對象

4、獲取當前用戶的session對象 currentUser.getSession()

	public Session getSession() {
        return getSession(true);
    }

	public Session getSession(boolean create) {
        if (this.session == null && create) {
            if (!isSessionCreationEnabled()) {
                throw ...
            }
            SessionContext sessionContext = createSessionContext();
            Session session = this.securityManager.start(sessionContext);
            this.session = decorate(session);
        }
        return this.session;
    }

上面說到Subject的實際類型是DelegatingSubject類型

創建session上下文SessionContext(SessionContext包含sessionId、host信息)

	protected SessionContext createSessionContext() {
        SessionContext sessionContext = new DefaultSessionContext();
        if (StringUtils.hasText(host)) {
            sessionContext.setHost(host);
        }
        return sessionContext;
    }

創建session

	public Session start(SessionContext context) throws AuthorizationException {
        return this.sessionManager.start(context);
    }

例子中是通過SessionManager是DefaultSessionManager,其創建的session是一個SimpleSession對象

裝飾session對象

	protected Session decorate(Session session) {
        ...
        return new StoppingAwareProxiedSession(session, this);
    }

StoppingAwareProxiedSession重寫了Session的stop方法,主要是在session由於過期等原因被移除的時候,同時也在DelegatingSubject中移除,以防止內存泄露

5、登錄currentUser.login(token)

	public void login(AuthenticationToken token) throws AuthenticationException {
		//從session中移除RUN_AS_PRINCIPALS_SESSION_KEY屬性
        clearRunAsIdentitiesInternal();
		//調用安全管理器進行登錄,會返回一個Subject對象
        Subject subject = securityManager.login(this, token);
        PrincipalCollection principals;
        String host = null;
        if (subject instanceof DelegatingSubject) {
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals == null || principals.isEmpty()) {
            throw ...
        }
		//更新當前的成員
        this.principals = principals;
        this.authenticated = true;
        if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
            this.host = host;
        }
		更新session
        Session session = subject.getSession(false);
        if (session != null) {
            this.session = decorate(session);
        } else {
            this.session = null;
        }
    }

可以看到主要的登錄邏輯有安全管理器來實現

	public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {//回調
                onFailedLogin(token, ae, subject);
            } catch...
            throw ae; //繼續往上拋
        }
		//【標記1】生成Subject
        Subject loggedIn = createSubject(token, info, subject);
		//回調
        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

這裏的異常繼續往上拋就可以給開發者捕獲

	public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

	public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw ...
        }
        AuthenticationInfo info;
        try {
            info = doAuthenticate(token);
            if (info == null) {//用戶不存在
                throw ...
            }
        } catch (Throwable t) {
            ...
            try {
                notifyFailure(token, ae);//通知監聽者
            } catch...
            throw ae;
        }
        notifySuccess(token, info);//通知監聽者

        return info;
    }

	protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

由於例子裏只提供了一個realm所以會調用doSingleRealmAuthentication方法

	protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
            throw ...
        }
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            throw ...
        }
        return info;
    }

最後是調用了realm的getAuthenticationInfo來完成驗證

	public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		//嘗試從緩存中獲取
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //緩存獲取失敗,則通過其他方式獲取,通常是查數據庫
            info = doGetAuthenticationInfo(token);
            if (token != null && info != null) {
				//根據是否需要緩存來進行緩存
                cacheAuthenticationInfoIfPossible(token, info);
            }
        }
        if (info != null) {//開始比對驗證
            assertCredentialsMatch(token, info);
        } else {
            //用戶不存在
        }
        return info;
    }

上面可以看到通過doGetAuthenticationInfo來獲取用戶信息,這也是我們平時自己實現realm要實現的方法。

比對驗證

	protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
		//使用驗證器進行驗證
        CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(token, info)) {
                //驗證失敗
                throw new IncorrectCredentialsException(msg);
            }
        } else {
			//不存在驗證器
            throw ...
        }
    }

利用驗證器來進行驗證

【標記1】如果都驗證通過了,就會創建一個Subject返回

	protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
		//創建了一個DefaultSubjectContext
        SubjectContext context = createSubjectContext();
        context.setAuthenticated(true);//設置已經認證
        context.setAuthenticationToken(token);
        context.setAuthenticationInfo(info);
        if (existing != null) {
            context.setSubject(existing);
        }
        return createSubject(context);
    }

這裏的SubjectContext會把認證的標記爲設置成true,然後就是創建Subject,這個上面已經分析過,最後會返回一個DelegatingSubject。安全管理器登錄邏輯就完成了。

接下里就是對安全管理器登錄邏輯返回的Subject信息進行復制到當前subject

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

        if (principals == null || principals.isEmpty()) {
            throw ...
        }
        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);//裝飾session
        } else {
            this.session = null;
        }

6、當前用戶登出currentUser.logout()

	public void logout() {
        try {
			//清楚session中的RUN_AS_PRINCIPALS_SESSION_KEY屬性
            clearRunAsIdentitiesInternal();
            this.securityManager.logout(this);
        } finally {//清空信息
            this.session = null;
            this.principals = null;
            this.authenticated = false;
        }
    }

	public void logout(Subject subject) {

        if (subject == null) {
            throw ...
        }

        beforeLogout(subject);//刪除RememberMe管理器中的相關信息

        PrincipalCollection principals = subject.getPrincipals();
        if (principals != null && !principals.isEmpty()) {
            Authenticator authc = getAuthenticator();
            if (authc instanceof LogoutAware) {
				//這裏可以清理緩存和通知監聽器
                ((LogoutAware) authc).onLogout(principals);
            }
        }

        try {
            delete(subject);//這裏調用subjectDAO的delete方法
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                String msg = "Unable to cleanly unbind Subject.  Ignoring (logging out).";
                log.debug(msg, e);
            }
        } finally {
            try {
                stopSession(subject);//設置session過期
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    String msg = "Unable to cleanly stop Session for Subject [" + subject.getPrincipal() + "] " +
                            "Ignoring (logging out).";
                    log.debug(msg, e);
                }
            }
        }
    }

總結:

shiro框架可以說是化繁爲簡,各種組件各司其職,分工明確,值得深入學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章