之前在研究Shiro 源码的过程中,发现Shiro 会对request、response、session 进行包装。 下面研究其包装过程以及原理。
Session是通过包装了request, 重写了其获取Session 的方法。 然后重写了一套Shiro 自己的Session 管理机制(这个session 和 Servlet的HeepSession 没有关系), 只是对外暴露的时候封装成一个ShiroHttpSession 对象(该对象内部包含Shiro 的Session), 最终ShiroHttpSession 相关的操作都会交给Shiro的Session。 Shiro的session 实现了一套自己的生成ID、创建Session、删除、修改、获取所有的等方法; 并且也有定时任务去处理过期的session 等策略。 并且SHiro 提供了可扩展的抽象类,基于抽象类可以快速实现Session 存到Redis 或其他操作。
1. 使用ServletContainerSessionManager 走原来Servlet的一套机制
1. shiro 配置
// 权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中 securityManager.setRealms(Lists.newArrayList(new CustomRealm())); return securityManager; }
2. 增加测试Controller, 查看相关类型
@GetMapping("/login2")
public String login2(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
System.out.println(request);
System.out.println(response);
System.out.println(session);
System.out.println("华丽的分割线1~~~~");
HttpServletRequest request1 = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response1 = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
HttpSession session1 = request1.getSession(false);
System.out.println(request1);
System.out.println(response1);
System.out.println(session1);
System.out.println("华丽的分割线2~~~~");
Subject subject = SecurityUtils.getSubject();
AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222");
subject.login(generateToken);
return "success";
}
3. shiro 配置该路径允许匿名访问
4. 测试
访问后日志如下:
org.apache.shiro.web.servlet.ShiroHttpServletRequest@7adc6ef1 org.apache.catalina.connector.ResponseFacade@4802cdcd org.apache.catalina.session.StandardSessionFacade@1374f85e 华丽的分割线1~~~~ org.apache.shiro.web.servlet.ShiroHttpServletRequest@7adc6ef1 org.apache.catalina.connector.ResponseFacade@4802cdcd org.apache.catalina.session.StandardSessionFacade@1374f85e 华丽的分割线2~~~~
可以看到默认对Request 进行了包装,Response和sesson 仍然使用原来Servlet 中使用的对象。
5. 原理
1. org.apache.shiro.web.mgt.DefaultWebSecurityManager#DefaultWebSecurityManager() 构造如下:
public DefaultWebSecurityManager() { super(); DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(webEvalutator); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); webEvalutator.setSessionManager(getSessionManager()); }
可以看到默认的sessionManager 是 org.apache.shiro.web.session.mgt.ServletContainerSessionManager:
类图图像:
源码如下:
public class ServletContainerSessionManager implements WebSessionManager { //TODO - complete JavaDoc //TODO - read session timeout value from web.xml public ServletContainerSessionManager() { } public Session start(SessionContext context) throws AuthorizationException { return createSession(context); } public Session getSession(SessionKey key) throws SessionException { if (!WebUtils.isHttp(key)) { String msg = "SessionKey must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(key); Session session = null; HttpSession httpSession = request.getSession(false); if (httpSession != null) { session = createSession(httpSession, request.getRemoteHost()); } return session; } private String getHost(SessionContext context) { String host = context.getHost(); if (host == null) { ServletRequest request = WebUtils.getRequest(context); if (request != null) { host = request.getRemoteHost(); } } return host; } /** * @since 1.0 */ protected Session createSession(SessionContext sessionContext) throws AuthorizationException { if (!WebUtils.isHttp(sessionContext)) { String msg = "SessionContext must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(sessionContext); HttpSession httpSession = request.getSession(); //SHIRO-240: DO NOT use the 'globalSessionTimeout' value here on the acquired session. //see: https://issues.apache.org/jira/browse/SHIRO-240 String host = getHost(sessionContext); return createSession(httpSession, host); } protected Session createSession(HttpSession httpSession, String host) { return new HttpServletSession(httpSession, host); } /** * This implementation always delegates to the servlet container for sessions, so this method returns * {@code true} always. * * @return {@code true} always * @since 1.2 */ public boolean isServletContainerSessions() { return true; } }
2. 代理代码查看:
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 是Shiro的过滤器执行的入口:
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); final Subject subject = createSubject(request, response); //noinspection unchecked 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; } if (t != null) { if (t instanceof ServletException) { throw (ServletException) t; } if (t instanceof IOException) { throw (IOException) t; } //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one: String msg = "Filtered request failed."; throw new ServletException(msg, t); } }
(1) 包装Request的代码: org.apache.shiro.web.servlet.AbstractShiroFilter#prepareServletRequest
protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) { ServletRequest toUse = request; if (request instanceof HttpServletRequest) { HttpServletRequest http = (HttpServletRequest) request; toUse = wrapServletRequest(http); } return toUse; } protected ServletRequest wrapServletRequest(HttpServletRequest orig) { return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions()); } protected boolean isHttpSessions() { return getSecurityManager().isHttpSessionMode(); }
1》org.apache.shiro.web.servlet.ShiroHttpServletRequest#ShiroHttpServletRequest 构造如下:
public class ShiroHttpServletRequest extends HttpServletRequestWrapper { public static final String COOKIE_SESSION_ID_SOURCE = "cookie"; public static final String URL_SESSION_ID_SOURCE = "url"; public static final String REFERENCED_SESSION_ID = ShiroHttpServletRequest.class.getName() + "_REQUESTED_SESSION_ID"; public static final String REFERENCED_SESSION_ID_IS_VALID = ShiroHttpServletRequest.class.getName() + "_REQUESTED_SESSION_ID_VALID"; public static final String REFERENCED_SESSION_IS_NEW = ShiroHttpServletRequest.class.getName() + "_REFERENCED_SESSION_IS_NEW"; public static final String REFERENCED_SESSION_ID_SOURCE = ShiroHttpServletRequest.class.getName() + "REFERENCED_SESSION_ID_SOURCE"; public static final String IDENTITY_REMOVED_KEY = ShiroHttpServletRequest.class.getName() + "_IDENTITY_REMOVED_KEY"; public static final String SESSION_ID_URL_REWRITING_ENABLED = ShiroHttpServletRequest.class.getName() + "_SESSION_ID_URL_REWRITING_ENABLED"; protected ServletContext servletContext = null; protected HttpSession session = null; protected boolean httpSessions = true; public ShiroHttpServletRequest(HttpServletRequest wrapped, ServletContext servletContext, boolean httpSessions) { super(wrapped); this.servletContext = servletContext; this.httpSessions = httpSessions; }
2》判断isHttpSessions 是否是 httpSession 的方法如下:
org.apache.shiro.web.mgt.DefaultWebSecurityManager#isHttpSessionMode:
public boolean isHttpSessionMode() { SessionManager sessionManager = getSessionManager(); return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions(); }
也就是判断是否是WebSessionManager, 然后调用 isServletContainerSessions 判断。 默认的ServletContainerSessionManager 返回true。 所以是HttpSessionMode。
(2) 包装Response 的代码 org.apache.shiro.web.servlet.AbstractShiroFilter#prepareServletResponse
protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) { ServletResponse toUse = response; if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) && (response instanceof HttpServletResponse)) { //the ShiroHttpServletResponse exists to support URL rewriting for session ids. This is only needed if //using Shiro sessions (i.e. not simple HttpSession based sessions): toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request); } return toUse; }
可以看到包装的条件是: !isHttpSessions() 并且request是 ShiroHttpServletRequest; 并且 response instanceof HttpServletResponse。 第一个条件不满足,所以不会进行包装。
3》 session 进行包装的条件:
createSubject(request, response); 创建Subject, 会进行解析相关session。
(1) 调用到: org.apache.shiro.mgt.DefaultSecurityManager#resolveContextSession
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException { SessionKey key = getSessionKey(context); if (key != null) { return getSession(key); } return null; }
- 调用 org.apache.shiro.web.mgt.DefaultWebSecurityManager#getSessionKey 获取SessionKey
protected SessionKey getSessionKey(SubjectContext context) { if (WebUtils.isWeb(context)) { Serializable sessionId = context.getSessionId(); ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); return new WebSessionKey(sessionId, request, response); } else { return super.getSessionKey(context); } }
第一次获取到的sessionId 为空,返回一个WebSessionKey 对象, sessionId 为空。
- 调用org.apache.shiro.mgt.SessionsSecurityManager#getSession 获取session
public Session getSession(SessionKey key) throws SessionException { return this.sessionManager.getSession(key); }
继续调用调用到:org.apache.shiro.web.session.mgt.ServletContainerSessionManager#getSession
public Session getSession(SessionKey key) throws SessionException { if (!WebUtils.isHttp(key)) { String msg = "SessionKey must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(key); Session session = null; HttpSession httpSession = request.getSession(false); if (httpSession != null) { session = createSession(httpSession, request.getRemoteHost()); } return session; } protected Session createSession(HttpSession httpSession, String host) { return new HttpServletSession(httpSession, host); }
继续调用到: org.apache.shiro.web.servlet.ShiroHttpServletRequest#getSession(boolean)
public HttpSession getSession(boolean create) { HttpSession httpSession; if (isHttpSessions()) { httpSession = super.getSession(false); if (httpSession == null && create) { //Shiro 1.2: assert that creation is enabled (SHIRO-266): if (WebUtils._isSessionCreationEnabled(this)) { httpSession = super.getSession(create); } else { throw newNoSessionCreationException(); } } } else { boolean existing = getSubject().getSession(false) != null; if (this.session == null || !existing) { Session shiroSession = getSubject().getSession(create); if (shiroSession != null) { this.session = new ShiroHttpSession(shiroSession, this, this.servletContext); if (!existing) { setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE); } } else if (this.session != null) { this.session = null; } } httpSession = this.session; } return httpSession; }
可以看到isHttpSessions 是true, 所以走上面不包装的代码逻辑。也就是所有的session 都走的原来session的一套机制。
2. 修改Sessionmanager为DefaultWebSessionManager走Shiro的机制
1. 修改配置
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中 securityManager.setRealms(Lists.newArrayList(new CustomRealm())); securityManager.setSessionManager(new DefaultWebSessionManager()); return securityManager; }
2. 测试
查看控制台如下:
org.apache.shiro.web.servlet.ShiroHttpServletRequest@675f5183 org.apache.shiro.web.servlet.ShiroHttpServletResponse@6998a318 org.apache.shiro.web.servlet.ShiroHttpSession@6da73af3 华丽的分割线1~~~~ org.apache.shiro.web.servlet.ShiroHttpServletRequest@675f5183 org.apache.shiro.web.servlet.ShiroHttpServletResponse@6998a318 org.apache.shiro.web.servlet.ShiroHttpSession@6da73af3 华丽的分割线2~~~~
可以看到对request、response、session 都进行了包装。查看相关对象如下:
(1) request:
(2) response:
(3) session:
4. 包装原理
1. org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 在Filter 内部, Shiro 对request、response 进行了包装。
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); final Subject subject = createSubject(request, response); //noinspection unchecked 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; } if (t != null) { if (t instanceof ServletException) { throw (ServletException) t; } if (t instanceof IOException) { throw (IOException) t; } //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one: String msg = "Filtered request failed."; throw new ServletException(msg, t); } }
prepareServletRequest 包装request; prepareServletResponse 包装response。 因为org.apache.shiro.web.session.mgt.DefaultWebSessionManager#isServletContainerSessions 返回是false, 所以取后是true。 那么满足包装的条件。
protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) { ServletResponse toUse = response; if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) && (response instanceof HttpServletResponse)) { //the ShiroHttpServletResponse exists to support URL rewriting for session ids. This is only needed if //using Shiro sessions (i.e. not simple HttpSession based sessions): toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request); } return toUse; }
2. 进行后续调用 executeChain(request, response, chain); 用的都是包装后的request, response。
3. 过滤器执行完后会进入Servlet 调用, 调用SpringMVC 的方法org.springframework.web.servlet.DispatcherServlet#doService 之前会先进入其父类 org.springframework.web.servlet.FrameworkServlet#processRequest:
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContext localeContext = buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor()); initContextHolders(request, localeContext, requestAttributes); try { doService(request, response); } catch (ServletException | IOException ex) { failureCause = ex; throw ex; } catch (Throwable ex) { failureCause = ex; throw new NestedServletException("Request processing failed", ex); } finally { resetContextHolders(request, previousLocaleContext, previousAttributes); if (requestAttributes != null) { requestAttributes.requestCompleted(); } logResult(request, response, failureCause, asyncManager); publishRequestHandledEvent(request, response, startTime, failureCause); } }
1》 调用 buildRequestAttributes(request, response, previousAttributes); 将Request、Response 维护起来, 这里的request和response 对象都是包装后的对象。
2》 org.springframework.web.servlet.FrameworkServlet#initContextHolders 将上面的对象保存到ThreadLocal 中, 也就是当前环境使用的request、response 以及传递到SpringMVC、Controller 的request、response 都是shiro 包装后的对象。
private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) { if (localeContext != null) { LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable); } if (requestAttributes != null) { RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); } }
RequestContextHolder 是Spring 记录请求上下文环境的对象,用的也是ThreadLocal 对象进行维护。
3. session管理
修改为DefaultWebSessionManager 之后,实际就是将session 交给shiro 管理。
1. 类图:
2. 重要类
1. org.apache.shiro.web.session.mgt.DefaultWebSessionManager#DefaultWebSessionManager 构造如下:
public DefaultWebSessionManager() { Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME); cookie.setHttpOnly(true); //more secure, protects against XSS attacks this.sessionIdCookie = cookie; this.sessionIdCookieEnabled = true; this.sessionIdUrlRewritingEnabled = true; }
sessionIdCookie 默认是读取名称为JSESSIONID 的cookie。
父类构造: org.apache.shiro.session.mgt.DefaultSessionManager#DefaultSessionManager 可以看到创建了一个 MemorySessionDAO
public DefaultSessionManager() { this.deleteInvalidSessions = true; this.sessionFactory = new SimpleSessionFactory(); this.sessionDAO = new MemorySessionDAO(); }
涉及到的重要的类如下:
(1) org.apache.shiro.session.mgt.SimpleSessionFactory 源码如下: 简单创建一个session
package org.apache.shiro.session.mgt; import org.apache.shiro.session.Session; /** * {@code SessionFactory} implementation that generates {@link SimpleSession} instances. * * @since 1.0 */ public class SimpleSessionFactory implements SessionFactory { /** * Creates a new {@link SimpleSession SimpleSession} instance retaining the context's * {@link SessionContext#getHost() host} if one can be found. * * @param initData the initialization data to be used during {@link Session} creation. * @return a new {@link SimpleSession SimpleSession} instance */ public Session createSession(SessionContext initData) { if (initData != null) { String host = initData.getHost(); if (host != null) { return new SimpleSession(host); } } return new SimpleSession(); } }
(2) org.apache.shiro.session.mgt.eis.SessionDAO 是一个接口,提供了session 的 增删改查
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import java.io.Serializable; import java.util.Collection; /** * Data Access Object design pattern specification to enable {@link Session} access to an * EIS (Enterprise Information System). It provides your four typical CRUD methods: * {@link #create}, {@link #readSession(java.io.Serializable)}, {@link #update(org.apache.shiro.session.Session)}, * and {@link #delete(org.apache.shiro.session.Session)}. * <p/> * The remaining {@link #getActiveSessions()} method exists as a support mechanism to pre-emptively orphaned sessions, * typically by {@link org.apache.shiro.session.mgt.ValidatingSessionManager ValidatingSessionManager}s), and should * be as efficient as possible, especially if there are thousands of active sessions. Large scale/high performance * implementations will often return a subset of the total active sessions and perform validation a little more * frequently, rather than return a massive set and infrequently validate. * * @since 0.1 */ public interface SessionDAO { /** * Inserts a new Session record into the underling EIS (e.g. Relational database, file system, persistent cache, * etc, depending on the DAO implementation). * <p/> * After this method is invoked, the {@link org.apache.shiro.session.Session#getId()} * method executed on the argument must return a valid session identifier. That is, the following should * always be true: * <pre> * Serializable id = create( session ); * id.equals( session.getId() ) == true</pre> * <p/> * Implementations are free to throw any exceptions that might occur due to * integrity violation constraints or other EIS related errors. * * @param session the {@link org.apache.shiro.session.Session} object to create in the EIS. * @return the EIS id (e.g. primary key) of the created {@code Session} object. */ Serializable create(Session session); /** * Retrieves the session from the EIS uniquely identified by the specified * {@code sessionId}. * * @param sessionId the system-wide unique identifier of the Session object to retrieve from * the EIS. * @return the persisted session in the EIS identified by {@code sessionId}. * @throws UnknownSessionException if there is no EIS record for any session with the * specified {@code sessionId} */ Session readSession(Serializable sessionId) throws UnknownSessionException; /** * Updates (persists) data from a previously created Session instance in the EIS identified by * {@code {@link Session#getId() session.getId()}}. This effectively propagates * the data in the argument to the EIS record previously saved. * <p/> * In addition to UnknownSessionException, implementations are free to throw any other * exceptions that might occur due to integrity violation constraints or other EIS related * errors. * * @param session the Session to update * @throws org.apache.shiro.session.UnknownSessionException * if no existing EIS session record exists with the * identifier of {@link Session#getId() session.getSessionId()} */ void update(Session session) throws UnknownSessionException; /** * Deletes the associated EIS record of the specified {@code session}. If there never * existed a session EIS record with the identifier of * {@link Session#getId() session.getId()}, then this method does nothing. * * @param session the session to delete. */ void delete(Session session); /** * Returns all sessions in the EIS that are considered active, meaning all sessions that * haven't been stopped/expired. This is primarily used to validate potential orphans. * <p/> * If there are no active sessions in the EIS, this method may return an empty collection or {@code null}. * <h4>Performance</h4> * This method should be as efficient as possible, especially in larger systems where there might be * thousands of active sessions. Large scale/high performance * implementations will often return a subset of the total active sessions and perform validation a little more * frequently, rather than return a massive set and validate infrequently. If efficient and possible, it would * make sense to return the oldest unstopped sessions available, ordered by * {@link org.apache.shiro.session.Session#getLastAccessTime() lastAccessTime}. * <h4>Smart Results</h4> * <em>Ideally</em> this method would only return active sessions that the EIS was certain should be invalided. * Typically that is any session that is not stopped and where its lastAccessTimestamp is older than the session * timeout. * <p/> * For example, if sessions were backed by a relational database or SQL-92 'query-able' enterprise cache, you might * return something similar to the results returned by this query (assuming * {@link org.apache.shiro.session.mgt.SimpleSession SimpleSession}s were being stored): * <pre> * select * from sessions s where s.lastAccessTimestamp < ? and s.stopTimestamp is null * </pre> * where the {@code ?} parameter is a date instance equal to 'now' minus the session timeout * (e.g. now - 30 minutes). * * @return a Collection of {@code Session}s that are considered active, or an * empty collection or {@code null} if there are no active sessions. */ Collection<Session> getActiveSessions(); }
org.apache.shiro.session.mgt.eis.AbstractSessionDAO 抽象dao:
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SimpleSession; import java.io.Serializable; /** * An abstract {@code SessionDAO} implementation that performs some sanity checks on session creation and reading and * allows for pluggable Session ID generation strategies if desired. The {@code SessionDAO} * {@link SessionDAO#update update} and {@link SessionDAO#delete delete} methods are left to * subclasses. * <h3>Session ID Generation</h3> * This class also allows for plugging in a {@link SessionIdGenerator} for custom ID generation strategies. This is * optional, as the default generator is probably sufficient for most cases. Subclass implementations that do use a * generator (default or custom) will want to call the * {@link #generateSessionId(org.apache.shiro.session.Session)} method from within their {@link #doCreate} * implementations. * <p/> * Subclass implementations that rely on the EIS data store to generate the ID automatically (e.g. when the session * ID is also an auto-generated primary key), they can simply ignore the {@code SessionIdGenerator} concept * entirely and just return the data store's ID from the {@link #doCreate} implementation. * * @since 1.0 */ public abstract class AbstractSessionDAO implements SessionDAO { /** * Optional SessionIdGenerator instance available to subclasses via the * {@link #generateSessionId(org.apache.shiro.session.Session)} method. */ private SessionIdGenerator sessionIdGenerator; /** * Default no-arg constructor that defaults the {@link #setSessionIdGenerator sessionIdGenerator} to be a * {@link org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator}. */ public AbstractSessionDAO() { this.sessionIdGenerator = new JavaUuidSessionIdGenerator(); } /** * Returns the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)} * method. Unless overridden by the {@link #setSessionIdGenerator(SessionIdGenerator)} method, the default instance * is a {@link JavaUuidSessionIdGenerator}. * * @return the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)} * method. */ public SessionIdGenerator getSessionIdGenerator() { return sessionIdGenerator; } /** * Sets the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)} * method. Unless overridden by this method, the default instance ss a {@link JavaUuidSessionIdGenerator}. * * @param sessionIdGenerator the {@code SessionIdGenerator} to use in the * {@link #generateSessionId(org.apache.shiro.session.Session)} method. */ public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { this.sessionIdGenerator = sessionIdGenerator; } /** * Generates a new ID to be applied to the specified {@code session} instance. This method is usually called * from within a subclass's {@link #doCreate} implementation where they assign the returned id to the session * instance and then create a record with this ID in the EIS data store. * <p/> * Subclass implementations backed by EIS data stores that auto-generate IDs during record creation, such as * relational databases, don't need to use this method or the {@link #getSessionIdGenerator() sessionIdGenerator} * attribute - they can simply return the data store's generated ID from the {@link #doCreate} implementation * if desired. * <p/> * This implementation uses the {@link #setSessionIdGenerator configured} {@link SessionIdGenerator} to create * the ID. * * @param session the new session instance for which an ID will be generated and then assigned * @return the generated ID to assign */ protected Serializable generateSessionId(Session session) { if (this.sessionIdGenerator == null) { String msg = "sessionIdGenerator attribute has not been configured."; throw new IllegalStateException(msg); } return this.sessionIdGenerator.generateId(session); } /** * Creates the session by delegating EIS creation to subclasses via the {@link #doCreate} method, and then * asserting that the returned sessionId is not null. * * @param session Session object to create in the EIS and associate with an ID. */ public Serializable create(Session session) { Serializable sessionId = doCreate(session); verifySessionId(sessionId); return sessionId; } /** * Ensures the sessionId returned from the subclass implementation of {@link #doCreate} is not null and not * already in use. * * @param sessionId session id returned from the subclass implementation of {@link #doCreate} */ private void verifySessionId(Serializable sessionId) { if (sessionId == null) { String msg = "sessionId returned from doCreate implementation is null. Please verify the implementation."; throw new IllegalStateException(msg); } } /** * Utility method available to subclasses that wish to * assign a generated session ID to the session instance directly. This method is not used by the * {@code AbstractSessionDAO} implementation directly, but it is provided so subclasses don't * need to know the {@code Session} implementation if they don't need to. * <p/> * This default implementation casts the argument to a {@link SimpleSession}, Shiro's default EIS implementation. * * @param session the session instance to which the sessionId will be applied * @param sessionId the id to assign to the specified session instance. */ protected void assignSessionId(Session session, Serializable sessionId) { ((SimpleSession) session).setId(sessionId); } /** * Subclass hook to actually persist the given <tt>Session</tt> instance to the underlying EIS. * * @param session the Session instance to persist to the EIS. * @return the id of the session created in the EIS (i.e. this is almost always a primary key and should be the * value returned from {@link org.apache.shiro.session.Session#getId() Session.getId()}. */ protected abstract Serializable doCreate(Session session); /** * Retrieves the Session object from the underlying EIS identified by <tt>sessionId</tt> by delegating to * the {@link #doReadSession(java.io.Serializable)} method. If {@code null} is returned from that method, an * {@link UnknownSessionException} will be thrown. * * @param sessionId the id of the session to retrieve from the EIS. * @return the session identified by <tt>sessionId</tt> in the EIS. * @throws UnknownSessionException if the id specified does not correspond to any session in the EIS. */ public Session readSession(Serializable sessionId) throws UnknownSessionException { Session s = doReadSession(sessionId); if (s == null) { throw new UnknownSessionException("There is no session with id [" + sessionId + "]"); } return s; } /** * Subclass implementation hook that retrieves the Session object from the underlying EIS or {@code null} if a * session with that ID could not be found. * * @param sessionId the id of the <tt>Session</tt> to retrieve. * @return the Session in the EIS identified by <tt>sessionId</tt> or {@code null} if a * session with that ID could not be found. */ protected abstract Session doReadSession(Serializable sessionId); }