001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.web.session.mgt;
020
021import org.apache.shiro.session.ExpiredSessionException;
022import org.apache.shiro.session.InvalidSessionException;
023import org.apache.shiro.session.Session;
024import org.apache.shiro.session.mgt.DefaultSessionManager;
025import org.apache.shiro.session.mgt.DelegatingSession;
026import org.apache.shiro.session.mgt.SessionContext;
027import org.apache.shiro.session.mgt.SessionKey;
028import org.apache.shiro.web.servlet.Cookie;
029import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
030import org.apache.shiro.web.servlet.ShiroHttpSession;
031import org.apache.shiro.web.servlet.SimpleCookie;
032import org.apache.shiro.web.util.WebUtils;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import javax.servlet.ServletRequest;
037import javax.servlet.ServletResponse;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040import java.io.Serializable;
041
042
043/**
044 * Web-application capable {@link org.apache.shiro.session.mgt.SessionManager SessionManager} implementation.
045 *
046 * @since 0.9
047 */
048public class DefaultWebSessionManager extends DefaultSessionManager implements WebSessionManager {
049
050    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultWebSessionManager.class);
051
052    private Cookie sessionIdCookie;
053    private boolean sessionIdCookieEnabled;
054    private boolean sessionIdUrlRewritingEnabled;
055
056    public DefaultWebSessionManager() {
057        Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
058        //more secure, protects against XSS attacks
059        cookie.setHttpOnly(true);
060        if (!Boolean.getBoolean(SECURE_COOKIE_DISABLED)) {
061            cookie.setSecure(true);
062        }
063        this.sessionIdCookie = cookie;
064        this.sessionIdCookieEnabled = true;
065        this.sessionIdUrlRewritingEnabled = false;
066    }
067
068    public Cookie getSessionIdCookie() {
069        return sessionIdCookie;
070    }
071
072    @SuppressWarnings({"UnusedDeclaration"})
073    public void setSessionIdCookie(Cookie sessionIdCookie) {
074        this.sessionIdCookie = sessionIdCookie;
075    }
076
077    public boolean isSessionIdCookieEnabled() {
078        return sessionIdCookieEnabled;
079    }
080
081    @SuppressWarnings({"UnusedDeclaration"})
082    public void setSessionIdCookieEnabled(boolean sessionIdCookieEnabled) {
083        this.sessionIdCookieEnabled = sessionIdCookieEnabled;
084    }
085
086    public boolean isSessionIdUrlRewritingEnabled() {
087        return sessionIdUrlRewritingEnabled;
088    }
089
090    @SuppressWarnings({"UnusedDeclaration"})
091    public void setSessionIdUrlRewritingEnabled(boolean sessionIdUrlRewritingEnabled) {
092        this.sessionIdUrlRewritingEnabled = sessionIdUrlRewritingEnabled;
093    }
094
095    private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
096        if (currentId == null) {
097            String msg = "sessionId cannot be null when persisting for subsequent requests.";
098            throw new IllegalArgumentException(msg);
099        }
100        Cookie template = getSessionIdCookie();
101        Cookie cookie = new SimpleCookie(template);
102        String idString = currentId.toString();
103        cookie.setValue(idString);
104        cookie.saveTo(request, response);
105        LOGGER.trace("Set session ID cookie for session with id {}", idString);
106    }
107
108    private void removeSessionIdCookie(HttpServletRequest request, HttpServletResponse response) {
109        getSessionIdCookie().removeFrom(request, response);
110    }
111
112    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
113        if (!isSessionIdCookieEnabled()) {
114            LOGGER.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
115            return null;
116        }
117        if (!(request instanceof HttpServletRequest)) {
118            LOGGER.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
119            return null;
120        }
121        HttpServletRequest httpRequest = (HttpServletRequest) request;
122        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
123    }
124
125    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
126
127        String id = getSessionIdCookieValue(request, response);
128        if (id != null) {
129            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
130                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
131        } else {
132            //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
133
134            //try the URI path segment parameters first:
135            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
136
137            if (id == null && request instanceof HttpServletRequest) {
138                //not a URI path segment parameter, try the query parameters:
139                String name = getSessionIdName();
140                HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
141                String queryString = httpServletRequest.getQueryString();
142                if (queryString != null && queryString.contains(name)) {
143                    id = request.getParameter(name);
144                }
145                if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
146                    //try lowercase:
147                    id = request.getParameter(name.toLowerCase());
148                }
149            }
150            if (id != null) {
151                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
152                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
153            }
154        }
155        if (id != null) {
156            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
157            //automatically mark it valid here.  If it is invalid, the
158            //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
159            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
160        }
161
162        // always set rewrite flag - SHIRO-361
163        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
164
165        return id;
166    }
167
168    //SHIRO-351
169    //also see http://cdivilly.wordpress.com/2011/04/22/java-servlets-uri-parameters/
170    //since 1.2.2
171    private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) {
172
173        if (!(servletRequest instanceof HttpServletRequest)) {
174            return null;
175        }
176        HttpServletRequest request = (HttpServletRequest) servletRequest;
177        String uri = request.getRequestURI();
178        if (uri == null) {
179            return null;
180        }
181
182        int queryStartIndex = uri.indexOf('?');
183        //get rid of the query string
184        if (queryStartIndex >= 0) {
185            uri = uri.substring(0, queryStartIndex);
186        }
187
188        //now check for path segment parameters:
189        int index = uri.indexOf(';');
190        if (index < 0) {
191            //no path segment params - return:
192            return null;
193        }
194
195        //there are path segment params, let's get the last one that may exist:
196        final String token = paramName + "=";
197
198        //uri now contains only the path segment params
199        uri = uri.substring(index + 1);
200
201        //we only care about the last JSESSIONID param:
202        index = uri.lastIndexOf(token);
203        if (index < 0) {
204            //no segment param:
205            return null;
206        }
207
208        uri = uri.substring(index + token.length());
209
210        //strip off any remaining segment params:
211        index = uri.indexOf(';');
212        if (index >= 0) {
213            uri = uri.substring(0, index);
214        }
215
216        //what remains is the value
217        return uri;
218    }
219
220    //since 1.2.1
221    private String getSessionIdName() {
222        String name = this.sessionIdCookie != null ? this.sessionIdCookie.getName() : null;
223        if (name == null) {
224            name = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
225        }
226        return name;
227    }
228
229    protected Session createExposedSession(Session session, SessionContext context) {
230        if (!WebUtils.isWeb(context)) {
231            return super.createExposedSession(session, context);
232        }
233        ServletRequest request = WebUtils.getRequest(context);
234        ServletResponse response = WebUtils.getResponse(context);
235        SessionKey key = new WebSessionKey(session.getId(), request, response);
236        return new DelegatingSession(this, key);
237    }
238
239    protected Session createExposedSession(Session session, SessionKey key) {
240        if (!WebUtils.isWeb(key)) {
241            return super.createExposedSession(session, key);
242        }
243
244        ServletRequest request = WebUtils.getRequest(key);
245        ServletResponse response = WebUtils.getResponse(key);
246        SessionKey sessionKey = new WebSessionKey(session.getId(), request, response);
247        return new DelegatingSession(this, sessionKey);
248    }
249
250    /**
251     * Stores the Session's ID, usually as a Cookie, to associate with future requests.
252     *
253     * @param session the session that was just {@link #createSession created}.
254     */
255    @Override
256    protected void onStart(Session session, SessionContext context) {
257        super.onStart(session, context);
258
259        if (!WebUtils.isHttp(context)) {
260            LOGGER.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response "
261                    + "pair. No session ID cookie will be set.");
262            return;
263
264        }
265        HttpServletRequest request = WebUtils.getHttpRequest(context);
266        HttpServletResponse response = WebUtils.getHttpResponse(context);
267
268        if (isSessionIdCookieEnabled()) {
269            Serializable sessionId = session.getId();
270            storeSessionId(sessionId, request, response);
271        } else {
272            LOGGER.debug("Session ID cookie is disabled.  No cookie has been set for new session with id {}", session.getId());
273        }
274
275        request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
276        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
277    }
278
279    @Override
280    public Serializable getSessionId(SessionKey key) {
281        Serializable id = super.getSessionId(key);
282        if (id == null && WebUtils.isWeb(key)) {
283            ServletRequest request = WebUtils.getRequest(key);
284            ServletResponse response = WebUtils.getResponse(key);
285            id = getSessionId(request, response);
286        }
287        return id;
288    }
289
290    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
291        return getReferencedSessionId(request, response);
292    }
293
294    @Override
295    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
296        super.onExpiration(s, ese, key);
297        onInvalidation(key);
298    }
299
300    @Override
301    protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
302        super.onInvalidation(session, ise, key);
303        onInvalidation(key);
304    }
305
306    private void onInvalidation(SessionKey key) {
307        ServletRequest request = WebUtils.getRequest(key);
308        if (request != null) {
309            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
310        }
311        if (WebUtils.isHttp(key)) {
312            LOGGER.debug("Referenced session was invalid.  Removing session ID cookie.");
313            removeSessionIdCookie(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
314        } else {
315            LOGGER.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response "
316                    + "pair. Session ID cookie will not be removed due to invalidated session.");
317        }
318    }
319
320    @Override
321    protected void onStop(Session session, SessionKey key) {
322        super.onStop(session, key);
323        if (WebUtils.isHttp(key)) {
324            HttpServletRequest request = WebUtils.getHttpRequest(key);
325            HttpServletResponse response = WebUtils.getHttpResponse(key);
326            LOGGER.debug("Session has been stopped (subject logout or explicit stop).  Removing session ID cookie.");
327            removeSessionIdCookie(request, response);
328        } else {
329            LOGGER.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response "
330                    + "pair. Session ID cookie will not be removed due to stopped session.");
331        }
332    }
333
334    /**
335     * This is a native session manager implementation, so this method returns {@code false} always.
336     *
337     * @return {@code false} always
338     * @since 1.2
339     */
340    public boolean isServletContainerSessions() {
341        return false;
342    }
343}