構建一個互聯(lián)網應用涉瘾,權限校驗管理是很重要的安全措施兴革,這其中主要包含:
- 認證 - 用戶身份識別壁拉,即登錄
- 授權 - 訪問控制
- 密碼加密 - 加密敏感數(shù)據防止被偷窺
- 會話管理 - 與用戶相關的時間敏感的狀態(tài)信息
Shiro對以上功能都進行了很好的支持兵志,而且十分易于使用,且可運行在注入WEB, IOC, EJB等環(huán)境中姜骡。
在Shiro中导坟,有以下幾個核心概念。
1. Subject
對于一個應用的權限校驗模塊來說圈澈,首先要考慮的就是“當前操作的用戶是誰”惫周, “是否允許該用戶進行某項操作”。因為應用接口都是基于用戶的某個基本操作來構建的康栈,所以我們構建一個應用的權限模塊递递,是基于用戶的概念來構建的。
Shiro的Subject概念就很好地基于用戶概念做了抽象啥么。Subject在Shiro中表示當前執(zhí)行操作的用戶登舞,這個用戶概念不僅僅是指由真實人類發(fā)起的某項請求,也可以使一個后臺線程悬荣、一個后臺帳戶或者是其他實體對象菠秒。
例如在Shiro中,我們可以通過如下代碼獲得一個Subject對象:
Subject currentUser = SecurityUtils.getSubject();
在獲取了Subject對象之后氯迂,就可以執(zhí)行包括登錄践叠、登出、獲取會話嚼蚀、權限校驗等操作禁灼。Shiro的簡單易用的API,使得我們在程序的任何地方都能很方便地獲取當前登錄用戶轿曙,并進行登錄用戶的各項基本操作弄捕。
Subject currentUser = SecurityUtils.getSubject();
currentUser.isAuthenticated()
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);
currentUser.hasRole("schwartz")
currentUser.isPermitted("lightsaber:wield")
currentUser.logout();
2.SecurityManager
通過ini的方式可以配置SecurityManager,里面包含用戶信息导帝、角色察藐、權限、url權限信息舟扎。SecurityManager通常是單例的分飞,因為新建需要讀取ini文件配置是耗時的,而且其只存儲相關配置信息睹限。
SecurityManager則管理所有用戶的安全操作譬猫,它是Shiro框架的核心。一旦其初始化配置完成羡疗,我們就不會再調用其相關API了染服,而是將精力集中在了Subject相關的權限操作上了。
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
# =======================
# Shiro INI configuration
# =======================
[main]
# Objects and their properties are defined here,
# Such as the securityManager, Realms and anything
# else needed to build the SecurityManager
iniRealm= org.apache.shiro.realm.text.IniRealm
securityManager.realms=iniRealm
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
[urls]
# The 'urls' section is used for url-based security
# in web applications. We'll discuss this section in the
# Web documentation
3.Realms
Realm充當了Shiro與應用安全數(shù)據間的橋梁叨恨。當用戶需要授權登錄時柳刮,Shiro使用Realms獲取授權驗證所必須的安全數(shù)據。所以,從本質上將秉颗,Realm實質上是一個安全相關的DAO痢毒,它封裝了數(shù)據源的連接細節(jié),并在需要時將相關數(shù)據提供給Shiro蚕甥。當配置Shiro時哪替,你必須至少指定一個Realm,用于認證和(或)授權菇怀。配置多個Realm是可以的凭舶,但是至少需要一個。
Apache Shiro提供多種認證數(shù)據源的支持爱沟,包括從JDBC, JNDI, LDAP等數(shù)據源獲取認證信息帅霜。
4.AuthenticationToken
AuthenticationToken
是用戶Subject提交的有關登錄主體和憑證的基本信息組合,這個token會通過Authenticator#authenticate(AuthenticationToken)
提交給Authenticator
呼伸,由Authenticator
執(zhí)行授權和登錄過程身冀。
同時,AuthenticationToken
有UsernamePasswordToken
的默認實現(xiàn)蜂大,如果我們程序是基本的通過用戶名+密碼的登錄方式闽铐,可以直接使用該類作為用戶登錄憑證的提交方式。
當然我們也可以通過implement AuthenticationToken的方式來實現(xiàn)自定義的登錄方式和特殊的必需登錄數(shù)據的索取奶浦。
下面從認證授權的全過程兄墅,來介紹Shiro的授權認證過程:
package com.zhuke.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//all done - log out!
currentUser.logout();
System.exit(0);
}
}
Subject currentUser = SecurityUtils.getSubject();
從當前線程獲取一個Subject授權對象,如果不存在澳叉,則新建一個隙咸。
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
可以看到,這里的Subject對象信息是儲存在ThreadContext中的成洗,那么我們對這個ThreadContext做一個簡單分析五督。
ThreadContext提供了一個在當前線程上綁定和解綁key/value鍵值對的操作。其內部使用了一個
ThreadLocal<Map<Object, Object>>
來存儲鍵值對瓶殃。
如果程序不想要線程之間共享信息(注入線程池或者線程復用等手段)充包,那么必須在調用棧開始和結束階段主動調用清理敏感信息(通過remove
方法)
//存儲線程獨占的key/value信息
private static final ThreadLocal<Map<Object, Object>> resources
= new InheritableThreadLocalMap<Map<Object, Object>>()
login的具體源碼方法為:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
//委托給securityManager執(zhí)行具體的登錄驗證工作
Subject subject = securityManager.login(this, token);
……
}
securityManager又將具體的授權驗證任務交給Authenticator
執(zhí)行:
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
而Authenticator
則會查找配置的所有realms,根據realms配置的授權驗證方案進行授權驗證:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
//獲取所有配置的realms
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
而通過以上對Realm的分析遥椿,我們知道Shiro有多個Realm的實現(xiàn)基矮,對于互聯(lián)網程序,通常情況我們將用戶名和密碼信息存儲在數(shù)據庫中冠场,在做授權驗證的時候家浇,從數(shù)據庫中取出用戶名和密碼進行比對。
下面將對Shiro的JDBCRealm進行分析碴裙。
JDBCRealm
其中定義了獲取存儲在數(shù)據庫中的用戶名|密碼|鹽值的相關sql語句钢悲。
/**
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
/**
* The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
*/
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
/**
* The default query used to retrieve the roles that apply to a user.
*/
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
/**
* The default query used to retrieve permissions that apply to a particular role.
*/
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
幾種鹽值存儲方案:
//NO_SALT - password hashes are not salted.
//CRYPT - password hashes are stored in unix crypt format.
//COLUMN - salt is in a separate column in the database.
//EXTERNAL - salt is not stored in the database. getSaltForUser(String) will be called to get the salt
public enum SaltStyle {NO_SALT, CRYPT, COLUMN, EXTERNAL};
具體執(zhí)行授權驗證的代碼為:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// Null username is invalid
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
Connection conn = null;
SimpleAuthenticationInfo info = null;
try {
conn = dataSource.getConnection();//獲取數(shù)據庫連接
String password = null;
String salt = null;
switch (saltStyle) {
case NO_SALT:
password = getPasswordForUser(conn, username)[0];
break;
case CRYPT:
……
case COLUMN:
……
case EXTERNAL:
……
}
if (password == null) {
throw new UnknownAccountException("No account found for user [" + username + "]");
}
info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
if (salt != null) {
info.setCredentialsSalt(ByteSource.Util.bytes(salt));
}
} catch (SQLException e) {
……
} finally {
JdbcUtils.closeConnection(conn);
}
return info;
}
在getPasswordForUser
內部執(zhí)行配置的authenticationQuery查找指定用戶名的密碼信息
private String[] getPasswordForUser(Connection conn, String username) throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
執(zhí)行配置的authenticationQuery語句查找指定用戶名的密碼信息
ps = conn.prepareStatement(authenticationQuery);
ps.setString(1, username);
// Execute query
rs = ps.executeQuery();
……
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(ps);
}
return result;
}
通過JDBCRealm配置的sql語句查找完成指定username的password, rolename, permission后点额,我們需要比對用戶提交的password和正確的password是否匹配。Shiro使用CredentialsMatcher
來計算上述的匹配關系莺琳。
SimpleCredentialsMatcher
其中SimpleCredentialsMatcher
簡單比較提交的密碼和真實密碼的byte流是否想等(密碼為:instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream
時)还棱,或者直接通過Object.equals比較(不滿足上訴條件時)
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
if (log.isDebugEnabled()) {
log.debug("Performing credentials equality check for tokenCredentials of type [" +
tokenCredentials.getClass().getName() + " and accountCredentials of type [" +
accountCredentials.getClass().getName() + "]");
}
if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
if (log.isDebugEnabled()) {
log.debug("Both credentials arguments can be easily converted to byte arrays. Performing " +
"array equals comparison");
}
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
return MessageDigest.isEqual(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
PasswordMatcher
PasswordMatcher
是Shiro推薦的用戶名密碼校驗的最佳實踐,因為他通過注入一個程序自定義的PasswordService
實現(xiàn)芦昔,來進行用戶名和密碼的授權校驗诱贿。
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
PasswordService service = ensurePasswordService();
Object submittedPassword = getSubmittedPassword(token);
Object storedCredentials = getStoredPassword(info);
assertStoredCredentialsType(storedCredentials);
if (storedCredentials instanceof Hash) {
Hash hashedPassword = (Hash)storedCredentials;
HashingPasswordService hashingService = assertHashingPasswordService(service);
return hashingService.passwordsMatch(submittedPassword, hashedPassword);
}
//otherwise they are a String (asserted in the 'assertStoredCredentialsType' method call above):
String formatted = (String)storedCredentials;
//調用注入的passwordService的實現(xiàn)來進行密碼的匹配校驗
return passwordService.passwordsMatch(submittedPassword, formatted);
}
程序通過實現(xiàn)PasswordService
來進行自定義的密碼校驗過程娃肿。
public interface PasswordService {
String encryptPassword(Object plaintextPassword) throws IllegalArgumentException;
boolean passwordsMatch(Object submittedPlaintext, String encrypted);
}
Session
Shiro提供一個權限的企業(yè)級Session解決方案咕缎,可以運行在簡單的命令行或者是智能手機平臺上,也可以工作在大型的集群應用上料扰。
以往我們需要使用Session的一些特性支持時凭豪,往往只能將服務部署在web容器或者EJB的Session特性。
Shiro的Session管理方案比上述兩種方案都更簡單晒杈,而且他可以運行在任何應用中嫂伞,與容器無關。
即使我們將應用部署在Servlet或者EJB容器中拯钻,Shiro Session的許多特性仍然值得我們使用它帖努。
- POJO/J2SE based (IoC friendly) - 在Shiro的應用框架中,所有都是基于接口的粪般。這使得我們可以很簡單快速地配置所有有關session的組件(通過JSON, YAML, Spring XML etc.)拼余。同時我們也能通過繼承Shiro的基本組件,實現(xiàn)我們自定義的session方案亩歹。
- Easy Custom Session Storage - 因為session對象是基于POJO的匙监,所以session數(shù)據可以很簡單方便地存儲在任意數(shù)據源中。比如:文件系統(tǒng)小作、分布式緩存亭姥、關系型數(shù)據庫等。
- Container-Independent Clustering - Shiro Session可以很方便地和目前成熟的緩存方案進行結合顾稀,比如 Ehcache + Terracotta, Coherence, GigaSpaces, et达罗。這意味著我們可以通過配置session存儲集群,使之和應用的部署容器無關静秆。
- 跨客戶端訪問 - 當我們使用EJB或者web 的session的時候粮揉,當我們要獲取session對象時,必須要在容器內才能獲得诡宗。Shiro通過在統(tǒng)一數(shù)據源(EhCache, redis, memcache etc)獲取到Session滔蝉,可以實現(xiàn)跨客戶端共享session數(shù)據。比如塔沃,一個java swing客戶端可以看到和共享web客戶端的同一用戶的session數(shù)據蝠引。
- Event Listeners - 事件監(jiān)聽機制允許我們監(jiān)聽session生命周期的全過程阳谍,并在相應事件發(fā)生時做出對應的反應。比如我們可以再一個用戶session過期時更新其對應的狀態(tài)信息螃概。
- Host Address Retention - Shiro Session保留了Session初始化時的原始IP和host name信息矫夯。這在互聯(lián)網環(huán)境下是十分有用的,我們可以根據用戶session的IP信息做出相應的反應和處理吊洼。
- Inactivity/Expiration Support - 我們可以通過
touch()
方法來延遲Session的過期训貌。- Transparent Web Use - Shiro基于Servlet 2.5實現(xiàn)了對HttpSession的完全支持。這就意味著我們可以適用Shiro Session在web 應用中冒窍,而不用更改任何其他代碼递沪。
- Can be used for SSO - 基于以上的:基于POJO, 可存儲在任意數(shù)據源综液, 可跨客戶端共享的特性款慨,我們可以用其實現(xiàn)一個基本的SSO。
在Shiro中谬莹,session的生命周期都在SessionManager中進行管理檩奠。
可以看到,Shiro的SecuityManager實現(xiàn)了SessionManager接口附帽,使其具有了管理session的能力埠戳,在Shiro中芳撒,Session的具體管理工作给郊,最終都實際委托給了默認的實現(xiàn)方案DefaultSessionManager
進行處理。
其中荒适,有以下兩個屬性慢显,在session的生命周期管理中起到了重要作用:
//session工廠類爪模,負責創(chuàng)建一個新的session對象
private SessionFactory sessionFactory;
//復雜對session進行CRUD的基本操作DAO類
protected SessionDAO sessionDAO;
下面從一個session的創(chuàng)建、存活荚藻、過期的生命周期從源碼層面來分析其設計方案屋灌。
首先,我們通過如下代碼獲取一個Session對象:
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
而Subject的session創(chuàng)建過程為:
public Session getSession(boolean create) {
//如果當前Subject的session為空应狱,且create=true共郭,則新建一個session
if (this.session == null && create) {
//如果配置的不允許新建session,則拋出異常
//added in 1.2:
if (!isSessionCreationEnabled()) {
String msg = "Session creation has been disabled for the current subject. This exception indicates " +
"that there is either a programming error (using a session when it should never be " +
"used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
"for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " +
"for more.";
throw new DisabledSessionException(msg);
}
log.trace("Starting session for host {}", getHost());
SessionContext sessionContext = createSessionContext();
//將session的創(chuàng)建委托給sessionManager執(zhí)行
Session session = this.securityManager.start(sessionContext);
this.session = decorate(session);
}
return this.session;
}
session初始化完成后疾呻,會調用sessionDAO.create()方法對新建的session進行分配sessionID和持久化的步驟除嘹。
sessionID的分配也是體現(xiàn)了Shiro中所有組件都使用接口的方式的設計理念,下面我們對其進行一個分析岸蜗。
處于最上層的SessionDAO接口定義了一個SessionDAOd的最基礎的方法尉咕,包括create為新建的session分配id和持久化,readSession根據id查找session璃岳,update更新session年缎, delete刪除session悔捶,getActiveSessions獲取所有正在生效的session。
而AbstractSessionDAO則在SessionDAO的基礎上单芜,實現(xiàn)了sessionID的分配方案蜕该。
通過注入不同的sessionID生成方案,我們可以對sessionID的分配方案進行自定義的差異化配置洲鸠。Shiro默認實現(xiàn)了兩種ID生成方案堂淡。
- 基于JAVA UUID:
public Serializable generateId(Session session) {
return UUID.randomUUID().toString();
}
- 基于
SHA1PRNG
的隨機算法
繼續(xù)回到SessionDAO的session持久化創(chuàng)建過程,通過可配置的sessionID分配方案分配完成sessionID后扒腕,會將session持久化到對應的數(shù)據源中绢淀。
這就有兩種選擇
- 單機共享的MemorySessionDAO
內部使用一個
ConcurrentMap<Serializable, Session> sessions
來存儲session
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
storeSession(sessionId, session);
return sessionId;
}
protected Session storeSession(Serializable id, Session session) {
if (id == null) {
throw new NullPointerException("id argument cannot be null.");
}
//以sessionID為key,session對象為值存入ConcurrentMap中
return sessions.putIfAbsent(id, session);
}
- 通過注入
CacheManager
實現(xiàn)session的透明化管理
通過向CachingSessionDAO注入一個
CacheManager
對象袜匿,由CacheManager提供Cache的獲取方案更啄,我們可以實現(xiàn)將session的管理交給CacheManager稚疹。
當然我們也可以通過繼承AbstractSessionDAO
居灯,實現(xiàn)其中具體的session的CRUD方法,來進行自定義數(shù)據源的session管理工作内狗。
如下怪嫌,通過繼承,我們成功將session持久化到了memcache數(shù)據源中柳沙。
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import net.spy.memcached.MemcachedClient;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 維持一個登錄會話的實現(xiàn)類岩灭,將會話信息存儲在緩存層
*/
public class LoginSessionDAO extends AbstractSessionDAO {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginSessionDAO.class);
@Autowired
private MemcachedClient client;
// session信息存儲在memcache中的前綴
private String prefix;
// 過期時間(單位:秒)
private long expTime;
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
LOGGER.error("[Session is null or session is null]");
return;
}
String key = genSessionId(session.getId());
boolean result = client.delete(key);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("[delete session {}] key={}", result ? "success" : "fail", key);
}
}
@Override
public Collection<Session> getActiveSessions() {
// 暫不支持
return Collections.emptyList();
}
@Override
public void update(Session session) throws UnknownSessionException {
if (session == null || session.getId() == null) {
LOGGER.error("[Session is null or session is null]");
return;
}
String key = genSessionId(session.getId());
// 將session對象序列化,采用java的對象序列化方式
client.set(key, JavaObjectSerializer.toByteArray(session), expTime * 1000);
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
// JSON.DEFAULT_GENERATE_FEATURE &=
// ~SerializerFeature.SkipTransientField
// .getMask();
String key = genSessionId(sessionId);
// 將session對象序列化赂鲤,采用java的對象序列化方式
boolean result = client.set(key, JavaObjectSerializer.toByteArray(session), expTime * 1000);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("[create session {}] key={}", result ? "success" : "fail", key);
}
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
LOGGER.error("[SessionId is null]");
return null;
}
Object sessionData = client.get(genSessionId(sessionId));
if (sessionData == null){
return null;
}else{
return (Session) JavaObjectSerializer.toObject(sessionData);
}
}
private String genSessionId(Serializable sessionId) {
return prefix + sessionId;
}
}
Session Listeners
在上面我們介紹Shiro Session的特性時噪径,提到我們可以通過session listener的方式,來監(jiān)聽session的生命周期全過程数初,那么Shiro是怎么實現(xiàn)的呢找爱?
見SessionManager的繼承體系圖中,AbstractSessionManager
定義了session的過期時間相關屬性的設置和獲取方法泡孩,而AbstractNativeSessionManager
則定義和實現(xiàn)了session生命周期監(jiān)聽器的相關功能车摄。
//監(jiān)聽器列表
private Collection<SessionListener> listeners;
public Session start(SessionContext context) {
……
notifyStart(session);//session創(chuàng)建完畢,通知監(jiān)聽器
……
}
//遍歷監(jiān)聽器列表仑鸥,調用onStart方法
protected void notifyStart(Session session) {
for (SessionListener listener : this.listeners) {
listener.onStart(session);
}
}
```java
SessionListener接口定義了session的完整生命周期的對應的動作吮播,通過實現(xiàn)SessionListener接口,我們可以對session的生命周期變化做出相應的動作響應眼俊。
![SessionListener](http://upload-images.jianshu.io/upload_images/3159214-63b8a2f31f37f0b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### Session過期時間和過期策略
Session的默認過期時間在```org.apache.shiro.session.mgt.AbstractSessionManager#DEFAULT_GLOBAL_SESSION_TIMEOUT```有配置意狠,為30min。
Session必須在校驗到其已經失效時疮胖,從存儲系統(tǒng)中進行刪除环戈,這保證了我們的session存儲數(shù)據源不會隨著時間的流逝誊役,而被大量已過期的無用session占滿。
為了性能考慮谷市,SessionManager只在根據sessionID獲取session時會檢查session的有效狀態(tài)蛔垢。那么當一個會話在建立之后,從此就再也沒有心得請求與服務器進行交互迫悠,此時這個session因為不會再經過有效性校驗的過程了鹏漆,那么該session就將一直存在于存儲系統(tǒng)中。此時成該會話為```orphans session```创泄,我將其以為**孤立會話**艺玲。
為了避免大量的孤立會話榨干存儲資源,Shiro提供了一種定期檢查的機制來對已過期的session進行刪除鞠抑。
當然如果我們是將session持久化到緩存數(shù)據庫中去饭聚,如redis, memcache搁拙,通過緩存數(shù)據庫的過期機制秒梳,可以保證session的過期剔除的特性。
Shiro的默認配置為使用```ExecutorServiceSessionValidationScheduler```來定期清理過期session箕速,其內部使用JDK的```ScheduledExecutorService```作為線程任務管理器來管理清理任務酪碘。
```java
public void enableSessionValidation() {
if (this.interval > 0l) {
this.service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName(threadNamePrefix + count.getAndIncrement());
return thread;
}
});
//每隔interval時間執(zhí)行一次run()定義的任務
this.service.scheduleAtFixedRate(this, interval, interval, TimeUnit.MILLISECONDS);
}
this.enabled = true;
}
public void run() {
if (log.isDebugEnabled()) {
log.debug("Executing session validation...");
}
long startTime = System.currentTimeMillis();
//將任務轉交給sessionManager進行session校驗
this.sessionManager.validateSessions();
long stopTime = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("Session validation completed successfully in " + (stopTime - startTime) + " milliseconds.");
}
}
Session屬性改變時的持久化過程
其中,SimpleSession
存儲了session的基本屬性信息盐茎,包括sessionID兴垦,過期時間,上次訪問時間字柠,host探越,屬性信息等。
在通過Subject新建session時窑业,根據基本的上下文信息钦幔,新建的是一個SimpleSession簡單對象,并不具備對象持久化的相關操作数冬。
public Session createSession(SessionContext initData) {
if (initData != null) {
String host = initData.getHost();
if (host != null) {
return new SimpleSession(host);
}
}
return new SimpleSession();
}
但是在新建完成簡單SimpleSession完成的返回路徑中节槐,會對SimpleSession的功能進行增強,這其中就用到了代理的設計模式拐纱。
public Session start(SessionContext context) {
Session session = createSession(context);
applyGlobalSessionTimeout(session);
onStart(session, context);
notifyStart(session);
//Don't expose the EIS-tier Session object to the client-tier:
//對SimpleSession對象進行代理增強铜异,使其在屬性進行了改變的時候,能夠對更新相應的持久化存儲數(shù)據
return createExposedSession(session, context);
}
protected Session createExposedSession(Session session, SessionContext context) {
return new DelegatingSession(this, new DefaultSessionKey(session.getId()));
}
而其中對session對象所有的查找和更新操作都是通過其sessionManager根據sessionID在數(shù)據源中進行查找得到的最新結果秸架,并將更新結果update到數(shù)據源中揍庄。
public Collection<Object> getAttributeKeys() throws InvalidSessionException {
return sessionManager.getAttributeKeys(key);
}
所以session的每一次查找或更新都會經過一次配置的數(shù)據源的查找或更新。
Session & Subject的狀態(tài)
如果我們需要構建有狀態(tài)的應用程序东抹,比如我們需要在用戶首次登錄成功后蚂子,維持其登錄狀態(tài)沃测,在登錄的有效期內都擁有其對應的訪問授權權限。
Shiro使用Subject對應的Session來存儲Subject的身份信息食茎,如Subject identity(PrincipalCollection
)和認證狀態(tài)(subject.isAuthenticated()
)蒂破,以便于在后面的連接和請求中使用。
應用程序可以從下次請求中獲取sessionID别渔,通過sessionID查找到Subject授權信息和Session信息附迷。
Serializable sessionId = //get from the inbound request or remote method invocation payload
Subject requestSubject = new Subject.Builder().sessionId(sessionId).buildSubject();
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
……
}
//方法中會將認證授權信息存儲在session中
Subject loggedIn = createSubject(token, info, subject);
//如果token設置了rememberme=true,且配置了rememberMeManager哎媚,則對登錄的principal加密后信息進行保存
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
public Subject createSubject(SubjectContext subjectContext) {
……
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
protected void save(Subject subject) {
this.subjectDAO.save(subject);
}
//作為一個session的屬性喇伯,持久化保存在session中
protected void saveToSession(Subject subject) {
//performs merge logic, only updating the Subject's session if it does not match the current state:
mergePrincipals(subject);
mergeAuthenticationState(subject);
}
protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
rememberMeSuccessfulLogin(token, info, subject);
}
protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
rmm.onSuccessfulLogin(subject, token, info);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onSuccessfulLogin. RememberMe services will not be " +
"performed for account [" + info + "].";
log.warn(msg, e);
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("This " + getClass().getName() + " instance does not have a " +
"[" + RememberMeManager.class.getName() + "] instance configured. RememberMe services " +
"will not be performed for account [" + info + "].");
}
}
}
我們也可以禁用Shiro對Subject授權信息的session保存方式,這樣我們每次請求都需要重新進行授權驗證拨与。
[main]
...
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
...
上面說到稻据,我們可以全局禁用通過session的方式來存儲Subject的授權信息,那么考慮如下情況:
如果是人類用戶登錄請求授權买喧,我們需要維持用戶的登錄信息捻悯,這時需要上述的Suject session特性;
如果是機器后臺調用(如API調用)岗喉,這類請求具有很大的不連續(xù)性秋度,那么我們就不需要在session中存儲Subject的授權信息;
如果通過某些特定渠道登錄的用戶需要存儲授權信息钱床,某些不需要呢。
如果我們需要實現(xiàn)上述所說的埠居,某些情況下需要查牌,某些情況下不需要存儲Subject授權信息,可以實現(xiàn)SessionStorageEvaluator
接口來對情況進行自定義滥壕。
public Subject save(Subject subject) {
//是否需要在session中存儲subject信息的計算算法
if (isSessionStorageEnabled(subject)) {
saveToSession(subject);
} else {
log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
"authentication state are expected to be initialized on every request or invocation.", subject);
}
return subject;
}
protected boolean isSessionStorageEnabled(Subject subject) {
return getSessionStorageEvaluator().isSessionStorageEnabled(subject);
}
//實現(xiàn)自己的計算方案
public boolean isSessionStorageEnabled(Subject subject) {
boolean enabled = false;
if (WebUtils.isWeb(Subject)) {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
//set 'enabled' based on the current request.
} else {
//not a web request - maybe a RMI or daemon invocation?
//set 'enabled' another way...
}
return enabled;
}
[main]
...
sessionStorageEvaluator = com.mycompany.shiro.subject.mgt.MySessionStorageEvaluator
securityManager.subjectDAO.sessionStorageEvaluator = $sessionStorageEvaluator
...
示例配置代碼:
https://github.com/zhuke1993/shiro_example
參考資料:
https://www.infoq.com/articles/apache-shiro
https://shiro.apache.org/get-started.html
https://shiro.apache.org/session-management.html