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.filter.authc;
020
021import org.apache.shiro.authc.AuthenticationException;
022import org.apache.shiro.authc.AuthenticationToken;
023import org.apache.shiro.authc.UsernamePasswordToken;
024import org.apache.shiro.subject.Subject;
025import org.apache.shiro.web.util.WebUtils;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import javax.servlet.ServletRequest;
030import javax.servlet.ServletResponse;
031import javax.servlet.http.HttpServletRequest;
032
033
034/**
035 * Requires the requesting user to be authenticated for the request to continue, and if they are not, forces the user
036 * to login via by redirecting them to the {@link #setLoginUrl(String) loginUrl} you configure.
037 * <p/>
038 * <p>This filter constructs a {@link UsernamePasswordToken UsernamePasswordToken} with the values found in
039 * {@link #setUsernameParam(String) username}, {@link #setPasswordParam(String) password},
040 * and {@link #setRememberMeParam(String) rememberMe} request parameters.  It then calls
041 * {@link org.apache.shiro.subject.Subject#login(org.apache.shiro.authc.AuthenticationToken) Subject.login(usernamePasswordToken)},
042 * effectively automatically performing a login attempt.  Note that the login attempt will only occur when the
043 * {@link #isLoginSubmission(javax.servlet.ServletRequest, javax.servlet.ServletResponse) isLoginSubmission(request,response)}
044 * is <code>true</code>, which by default occurs when the request is for the {@link #setLoginUrl(String) loginUrl} and
045 * is a POST request.
046 * <p/>
047 * <p>If the login attempt fails, the resulting <code>AuthenticationException</code> fully qualified class name will
048 * be set as a request attribute under the {@link #setFailureKeyAttribute(String) failureKeyAttribute} key.  This
049 * FQCN can be used as an i18n key or lookup mechanism to explain to the user why their login attempt failed
050 * (e.g. no account, incorrect password, etc).
051 * <p/>
052 * <p>If you would prefer to handle the authentication validation and login in your own code, consider using the
053 * {@link PassThruAuthenticationFilter} instead, which allows requests to the
054 * {@link #loginUrl} to pass through to your application's code directly.
055 *
056 * @see PassThruAuthenticationFilter
057 * @since 0.9
058 */
059public class FormAuthenticationFilter extends AuthenticatingFilter {
060
061    //TODO - complete JavaDoc
062
063    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = "shiroLoginFailure";
064
065    public static final String DEFAULT_USERNAME_PARAM = "username";
066    public static final String DEFAULT_PASSWORD_PARAM = "password";
067    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";
068
069    private static final Logger log = LoggerFactory.getLogger(FormAuthenticationFilter.class);
070
071    private String usernameParam = DEFAULT_USERNAME_PARAM;
072    private String passwordParam = DEFAULT_PASSWORD_PARAM;
073    private String rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;
074
075    private String failureKeyAttribute = DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;
076
077    public FormAuthenticationFilter() {
078        setLoginUrl(DEFAULT_LOGIN_URL);
079    }
080
081    @Override
082    public void setLoginUrl(String loginUrl) {
083        String previous = getLoginUrl();
084        if (previous != null) {
085            this.appliedPaths.remove(previous);
086        }
087        super.setLoginUrl(loginUrl);
088        if (log.isTraceEnabled()) {
089            log.trace("Adding login url to applied paths.");
090        }
091        this.appliedPaths.put(getLoginUrl(), null);
092    }
093
094    public String getUsernameParam() {
095        return usernameParam;
096    }
097
098    /**
099     * Sets the request parameter name to look for when acquiring the username.  Unless overridden by calling this
100     * method, the default is <code>username</code>.
101     *
102     * @param usernameParam the name of the request param to check for acquiring the username.
103     */
104    public void setUsernameParam(String usernameParam) {
105        this.usernameParam = usernameParam;
106    }
107
108    public String getPasswordParam() {
109        return passwordParam;
110    }
111
112    /**
113     * Sets the request parameter name to look for when acquiring the password.  Unless overridden by calling this
114     * method, the default is <code>password</code>.
115     *
116     * @param passwordParam the name of the request param to check for acquiring the password.
117     */
118    public void setPasswordParam(String passwordParam) {
119        this.passwordParam = passwordParam;
120    }
121
122    public String getRememberMeParam() {
123        return rememberMeParam;
124    }
125
126    /**
127     * Sets the request parameter name to look for when acquiring the rememberMe boolean value.  Unless overridden
128     * by calling this method, the default is <code>rememberMe</code>.
129     * <p/>
130     * RememberMe will be <code>true</code> if the parameter value equals any of those supported by
131     * {@link org.apache.shiro.web.util.WebUtils#isTrue(javax.servlet.ServletRequest, String) WebUtils.isTrue(request,value)}, <code>false</code>
132     * otherwise.
133     *
134     * @param rememberMeParam the name of the request param to check for acquiring the rememberMe boolean value.
135     */
136    public void setRememberMeParam(String rememberMeParam) {
137        this.rememberMeParam = rememberMeParam;
138    }
139
140    public String getFailureKeyAttribute() {
141        return failureKeyAttribute;
142    }
143
144    public void setFailureKeyAttribute(String failureKeyAttribute) {
145        this.failureKeyAttribute = failureKeyAttribute;
146    }
147
148    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
149        if (isLoginRequest(request, response)) {
150            if (isLoginSubmission(request, response)) {
151                if (log.isTraceEnabled()) {
152                    log.trace("Login submission detected.  Attempting to execute login.");
153                }
154                return executeLogin(request, response);
155            } else {
156                if (log.isTraceEnabled()) {
157                    log.trace("Login page view.");
158                }
159                //allow them to see the login page ;)
160                return true;
161            }
162        } else {
163            if (log.isTraceEnabled()) {
164                log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
165                        "Authentication url [" + getLoginUrl() + "]");
166            }
167
168            saveRequestAndRedirectToLogin(request, response);
169            return false;
170        }
171    }
172
173    /**
174     * This default implementation merely returns <code>true</code> if the request is an HTTP <code>POST</code>,
175     * <code>false</code> otherwise. Can be overridden by subclasses for custom login submission detection behavior.
176     *
177     * @param request  the incoming ServletRequest
178     * @param response the outgoing ServletResponse.
179     * @return <code>true</code> if the request is an HTTP <code>POST</code>, <code>false</code> otherwise.
180     */
181    @SuppressWarnings({"UnusedDeclaration"})
182    protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
183        return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
184    }
185
186    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
187        String username = getUsername(request);
188        String password = getPassword(request);
189        return createToken(username, password, request, response);
190    }
191
192    protected boolean isRememberMe(ServletRequest request) {
193        return WebUtils.isTrue(request, getRememberMeParam());
194    }
195
196    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
197                                     ServletRequest request, ServletResponse response) throws Exception {
198        issueSuccessRedirect(request, response);
199        //we handled the success redirect directly, prevent the chain from continuing:
200        return false;
201    }
202
203    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
204                                     ServletRequest request, ServletResponse response) {
205        if (log.isDebugEnabled()) {
206            log.debug( "Authentication exception", e );
207        }
208        setFailureAttribute(request, e);
209        //login failed, let request continue back to the login page:
210        return true;
211    }
212
213    protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
214        String className = ae.getClass().getName();
215        request.setAttribute(getFailureKeyAttribute(), className);
216    }
217
218    protected String getUsername(ServletRequest request) {
219        return WebUtils.getCleanParam(request, getUsernameParam());
220    }
221
222    protected String getPassword(ServletRequest request) {
223        return WebUtils.getCleanParam(request, getPasswordParam());
224    }
225
226
227}