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 */
019 package org.tynamo.security.shiro;
020
021 import static org.apache.shiro.util.StringUtils.split;
022
023 import java.io.IOException;
024
025 import javax.servlet.ServletRequest;
026 import javax.servlet.ServletResponse;
027
028 import org.apache.shiro.SecurityUtils;
029 import org.apache.shiro.subject.Subject;
030 import org.apache.shiro.util.AntPathMatcher;
031 import org.apache.shiro.util.PatternMatcher;
032 import org.apache.shiro.web.servlet.AdviceFilter;
033 import org.apache.shiro.web.util.WebUtils;
034
035 /**
036 * Superclass for any filter that controls access to a resource and may redirect the user to the login page
037 * if they are not authenticated. This superclass provides the method
038 * {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
039 * which is used by many subclasses as the behavior when a user is unauthenticated.
040 *
041 * This class and the subclasses that are used as Shiro's built-in filters were copied from Shiro 1.1.0
042 * and modified locally to implement same behavior as specified in https://issues.apache.org/jira/browse/SHIRO-256
043 * We'll revert to using Shiro's filters if the feature gets implemented in Shiro 2.x
044 *
045 * @since 0.4.0
046 */
047 public abstract class AccessControlFilter extends AdviceFilter {
048 // default values - populated by the ChainFactory from symbols
049 public static String LOGIN_URL, SUCCESS_URL, UNAUTHORIZED_URL;
050
051 protected PatternMatcher pathMatcher = new AntPathMatcher() {
052 @Override
053 public boolean match(String pattern, String string) {
054 return super.match(pattern, string.toLowerCase());
055 }
056 };
057
058 private String[] configElements;
059
060 private String config;
061
062 /*
063 * Same as {@link #setConfig()} except throws an {@link IllegalArgumentException} if config is already set.
064 * Mostly used in the initialization logic
065 */
066 public void addConfig(String config) {
067 if (config == null && this.config == null && configElements != null) return;
068 if (config != null && config.equals(this.config)) return;
069 if (configElements != null) throw new IllegalArgumentException("Configuration is already add for this filter, existing config is " + this.configElements + ". Use setConfig if you want to override the existing configuration");
070 setConfig(config);
071 }
072
073
074 public void setConfig(String config) {
075 if (config != null) configElements = split(config);
076 else configElements = new String[0];
077 this.config = config;
078 }
079
080 /**
081 * Constant representing the HTTP 'GET' request method, equal to <code>GET</code>.
082 */
083 public static final String GET_METHOD = "GET";
084
085 /**
086 * Constant representing the HTTP 'POST' request method, equal to <code>POST</code>.
087 */
088 public static final String POST_METHOD = "POST";
089
090 /**
091 * The login url to used to authenticate a user, used when redirecting users if authentication is required.
092 */
093 private String loginUrl = LOGIN_URL;
094
095 private String successUrl = SUCCESS_URL;
096
097 private String unauthorizedUrl = UNAUTHORIZED_URL;
098
099 /**
100 * Returns the success url to use as the default location a user is sent after logging in. Typically a redirect
101 * after login will redirect to the originally request URL; this property is provided mainly as a fallback in case
102 * the original request URL is not available or not specified.
103 * <p/>
104 * The default value is {@link #DEFAULT_SUCCESS_URL}.
105 *
106 * @return the success url to use as the default location a user is sent after logging in.
107 */
108 public String getSuccessUrl() {
109 return successUrl;
110 }
111
112 /**
113 * Sets the default/fallback success url to use as the default location a user is sent after logging in. Typically
114 * a redirect after login will redirect to the originally request URL; this property is provided mainly as a
115 * fallback in case the original request URL is not available or not specified.
116 * <p/>
117 * The default value is {@link #DEFAULT_SUCCESS_URL}.
118 *
119 * @param successUrl the success URL to redirect the user to after a successful login.
120 */
121 public void setSuccessUrl(String successUrl) {
122 this.successUrl = successUrl;
123 }
124
125
126 /**
127 * Returns the login URL used to authenticate a user.
128 * <p/>
129 * Most Shiro filters use this url
130 * as the location to redirect a user when the filter requires authentication. Unless overridden, the
131 * {@link #DEFAULT_LOGIN_URL DEFAULT_LOGIN_URL} is assumed, which can be overridden via
132 * {@link #setLoginUrl(String) setLoginUrl}.
133 *
134 * @return the login URL used to authenticate a user, used when redirecting users if authentication is required.
135 */
136 public String getLoginUrl() {
137 return loginUrl;
138 }
139
140 /**
141 * Sets the login URL used to authenticate a user.
142 * <p/>
143 * Most Shiro filters use this url as the location to redirect a user when the filter requires
144 * authentication. Unless overridden, the {@link #DEFAULT_LOGIN_URL DEFAULT_LOGIN_URL} is assumed.
145 *
146 * @param loginUrl the login URL used to authenticate a user, used when redirecting users if authentication is required.
147 */
148 public void setLoginUrl(String loginUrl) {
149 this.loginUrl = loginUrl;
150 }
151
152 public String getUnauthorizedUrl() {
153 return unauthorizedUrl;
154 }
155
156 public void setUnauthorizedUrl(String unauthorizedUrl) {
157 this.unauthorizedUrl = unauthorizedUrl;
158 }
159
160 /**
161 * Convenience method that acquires the Subject associated with the request.
162 * <p/>
163 * The default implementation simply returns
164 * {@link org.apache.shiro.SecurityUtils#getSubject() SecurityUtils.getSubject()}.
165 *
166 * @param request the incoming <code>ServletRequest</code>
167 * @param response the outgoing <code>ServletResponse</code>
168 * @return the Subject associated with the request.
169 */
170 protected Subject getSubject(ServletRequest request, ServletResponse response) {
171 return SecurityUtils.getSubject();
172 }
173
174 /**
175 * Returns <code>true</code> if the request is allowed to proceed through the filter normally, or <code>false</code>
176 * if the request should be handled by the
177 * {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(request,response,mappedValue)}
178 * method instead.
179 *
180 * @param request the incoming <code>ServletRequest</code>
181 * @param response the outgoing <code>ServletResponse</code>
182 * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
183 * @return <code>true</code> if the request should proceed through the filter normally, <code>false</code> if the
184 * request should be processed by this filter's
185 * {@link #onAccessDenied(ServletRequest,ServletResponse,Object)} method instead.
186 * @throws Exception if an error occurs during processing.
187 */
188 protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
189
190 /**
191 * Processes requests where the subject was denied access as determined by the
192 * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed}
193 * method, retaining the {@code mappedValue} that was used during configuration.
194 * <p/>
195 * This method immediately delegates to {@link #onAccessDenied(ServletRequest,ServletResponse)} as a
196 * convenience in that most post-denial behavior does not need the mapped config again.
197 *
198 * @param request the incoming <code>ServletRequest</code>
199 * @param response the outgoing <code>ServletResponse</code>
200 * @param mappedValue the config specified for the filter in the matching request's filter chain.
201 * @return <code>true</code> if the request should continue to be processed; false if the subclass will
202 * handle/render the response directly.
203 * @throws Exception if there is an error processing the request.
204 * @since 1.0
205 */
206 protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
207 return onAccessDenied(request, response);
208 }
209
210 /**
211 * Processes requests where the subject was denied access as determined by the
212 * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed}
213 * method.
214 *
215 * @param request the incoming <code>ServletRequest</code>
216 * @param response the outgoing <code>ServletResponse</code>
217 * @return <code>true</code> if the request should continue to be processed; false if the subclass will
218 * handle/render the response directly.
219 * @throws Exception if there is an error processing the request.
220 */
221 protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
222
223 /**
224 * Returns <code>true</code> if
225 * {@link #isAccessAllowed(ServletRequest,ServletResponse,Object) isAccessAllowed(Request,Response,Object)},
226 * otherwise returns the result of
227 * {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(Request,Response,Object)}.
228 *
229 * @return <code>true</code> if
230 * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed},
231 * otherwise returns the result of
232 * {@link #onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse) onAccessDenied}.
233 * @throws Exception if an error occurs.
234 */
235 public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
236 return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
237 }
238
239 protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
240 return onPreHandle(request, response, configElements);
241 }
242
243 /**
244 * Returns <code>true</code> if the incoming request is a login request, <code>false</code> otherwise.
245 * <p/>
246 * The default implementation merely returns <code>true</code> if the incoming request matches the configured
247 * {@link #getLoginUrl() loginUrl} by calling
248 * <code>{@link #pathsMatch(String, String) pathsMatch(loginUrl, request)}</code>.
249 *
250 * @param request the incoming <code>ServletRequest</code>
251 * @param response the outgoing <code>ServletResponse</code>
252 * @return <code>true</code> if the incoming request is a login request, <code>false</code> otherwise.
253 */
254 protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
255 return pathMatcher.matches(getLoginUrl(), WebUtils.getPathWithinApplication(WebUtils.toHttp(request)));
256 }
257
258 /**
259 * Convenience method for subclasses to use when a login redirect is required.
260 * <p/>
261 * This implementation simply calls {@link #saveRequest(javax.servlet.ServletRequest) saveRequest(request)}
262 * and then {@link #redirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse) redirectToLogin(request,response)}.
263 *
264 * @param request the incoming <code>ServletRequest</code>
265 * @param response the outgoing <code>ServletResponse</code>
266 * @throws IOException if an error occurs.
267 */
268 protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
269 saveRequest(request);
270 redirectToLogin(request, response);
271 }
272
273 /**
274 * Convenience method merely delegates to
275 * {@link WebUtils#saveRequest(javax.servlet.ServletRequest) WebUtils.saveRequest(request)} to save the request
276 * state for reuse later. This is mostly used to retain user request state when a redirect is issued to
277 * return the user to their originally requested url/resource.
278 * <p/>
279 * If you need to save and then immediately redirect the user to login, consider using
280 * {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
281 * saveRequestAndRedirectToLogin(request,response)} directly.
282 *
283 * @param request the incoming ServletRequest to save for re-use later (for example, after a redirect).
284 */
285 protected void saveRequest(ServletRequest request) {
286 WebUtils.saveRequest(request);
287 }
288
289 /**
290 * Convenience method for subclasses that merely acquires the {@link #getLoginUrl() getLoginUrl} and redirects
291 * the request to that url.
292 * <p/>
293 * <b>N.B.</b> If you want to issue a redirect with the intention of allowing the user to then return to their
294 * originally requested URL, don't use this method directly. Instead you should call
295 * {@link #saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
296 * saveRequestAndRedirectToLogin(request,response)}, which will save the current request state so that it can
297 * be reconstructed and re-used after a successful login.
298 *
299 * @param request the incoming <code>ServletRequest</code>
300 * @param response the outgoing <code>ServletResponse</code>
301 * @throws IOException if an error occurs.
302 */
303 protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
304 String loginUrl = getLoginUrl();
305 WebUtils.issueRedirect(request, response, loginUrl);
306 }
307
308 }