去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框架可以說是化繁爲簡,各種組件各司其職,分工明確,值得深入學習。