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