我们使用SpringSecurity进行配置的时候,有三种方式实现认证失败时的后续处理:其一,通过failureUrl()配置认证失败的重定向路径(Redirect);其二,我们还可以通过failureForwardUrl()配置认证失败的转发路径(Forward),和重定向效果类似,区别主要在于前者是重定向(默认),后者是转发;其三,自定义认证失败处理器,主要通过实现AuthenticationFailureHandler接口实现,其实前面两种方式也是通过实现该接口实现的。
failureUrl()配置方式:
//SpringSecurity配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/error","/goLogin","/doLogin","/401","/static/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/goLogin")
.loginProcessingUrl("/doLogin")
.failureUrl("/login/error");
}
failureForwardUrl()配置方式,和failureUrl()方法类似:
//SpringSecurity配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/error","/goLogin","/doLogin","/401","/static/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/goLogin")
.loginProcessingUrl("/doLogin")
.failureForwardUrl("/login/error");
}
自定义认证失败处理器,其实就是定义一个实现AuthenticationFailureHandler接口的处理类,然后通过failureHandler()方法进行注册就可以了,实现如下:
//SpringSecurity配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/error","/goLogin","/doLogin","/401","/static/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/goLogin")
.loginProcessingUrl("/doLogin")
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//处理逻辑
}
});
}
AuthenticationFailureHandler是认证失败后的处理接口,通过实现该接口,可以定义各类复杂的处理方式。默认的行为是,认证失败时,会重定向到登录页面。
AuthenticationFailureHandler类结构如下:
AuthenticationFailureHandler接口定义如下,其中onAuthenticationFailure()方法就是约定了认证失败后,将要执行的方法,该方法会在AbstractAuthenticationProcessingFilter类的unsuccessfulAuthentication()方法中调用。
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException;
}
failureForwardUrl()方法和failureUrl()方法是类似的,不过failureForwardUrl()方法使用的是转发技术,由服务端触发跳转的。底层的实现,还是通过实现AuthenticationFailureHandler接口实现,具体实现如下:
//FormLoginConfigurer.java
public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) {
failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));
return this;
}
//AbstractAuthenticationFilterConfigurer.java,是FormLoginConfigurer的父类
public final T failureHandler(
AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return getSelf();
}
通过上述代码,我们知道:failureForwardUrl()方法就是创建了一个ForwardAuthenticationFailureHandler对象,然后把该对象赋值给了failureHandler 变量,而ForwardAuthenticationFailureHandler就是AuthenticationFailureHandler接口的实现类,具体实现如下:
public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String forwardUrl;
public ForwardAuthenticationFailureHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl),
() -> "'" + forwardUrl + "' is not a valid forward URL");
this.forwardUrl = forwardUrl;
}
//通过foward方式实现跳转
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
request.getRequestDispatcher(forwardUrl).forward(request, response);
}
}
failureUrl()方法是通过浏览器重定向,实现认证失败后的跳转的,其实failureUrl()方法也可以实现和failureForwardUrl()方法一模一样的功能,我们一步步进行分析。
首先,failureUrl()方法的实现,和failureForwardUrl()方法类似,不过这里使用的是SimpleUrlAuthenticationFailureHandler处理器,具体如下:
//AbstractAuthenticationFilterConfigurer.java
public final T failureUrl(String authenticationFailureUrl) {
T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(
authenticationFailureUrl));
this.failureUrl = authenticationFailureUrl;
return result;
}
通过上述代码,我们可以清楚的知道使用了SimpleUrlAuthenticationFailureHandler对象作为处理,我们下面分析一下该处理器的onAuthenticationFailure()方法,实现如下:
//SimpleUrlAuthenticationFailureHandler.java
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}else {
//保存异常信息
saveException(request, exception);
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
SimpleUrlAuthenticationFailureHandler的onAuthenticationFailure()方法其实提供了重定向(默认)和转发两种实现方式,通过forwardToDestination变量进行控制。
自定义处理器方式其实就是认证失败处理流程的最基本的实现方式,前面两种也是SpringSecurity框架基于这种方式提供了两种常用的方案而已。配置方法实现如下:
public final T failureHandler(
AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return getSelf();
}
这里也是直接把自定义的处理器直接赋值给了failureHandler 变量,等待后续使用。
该类主要是为了适配AuthenticationEntryPoint实现,其中onAuthenticationFailure()方法其实就是由AuthenticationEntryPoint实现类的commence()方法实现,具体代码如下:
public class AuthenticationEntryPointFailureHandler implements AuthenticationFailureHandler {
private final AuthenticationEntryPoint authenticationEntryPoint;
public AuthenticationEntryPointFailureHandler(AuthenticationEntryPoint authenticationEntryPoint) {
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
this.authenticationEntryPoint.commence(request, response, exception);
}
}
如果我们没有配置任何认证失败处理相关内容,当我们输入错误的用户名或密码的时候,我们会发现会重新定向到登录页面,而且路径会添加上"?error"字符,例如"http://localhost:8888/qriver-admin/goLogin?error",为什么会这样呢?我们下面一步步进行分析。
在启动项目的时候,如果我们没有进行认证失败处理器的配置,系统会默认为我们配置一个,该方法主要在AbstractAuthenticationFilterConfigurer类的updateAuthenticationDefaults()方法中实现,在该方法中其实定义了loginProcessingUrl、failureHandler和loginPage三类默认配置,具体实现如下:
//AbstractAuthenticationFilterConfigurer.java
protected final void updateAuthenticationDefaults() {
if (loginProcessingUrl == null) {
loginProcessingUrl(loginPage);
}
if (failureHandler == null) {
failureUrl(loginPage + "?error");
}
final LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(
LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(loginPage + "?logout");
}
}
上述updateAuthenticationDefaults()方法会在init()和loginPage()两个方法中被调用。如果我们通过loginPage()配置了自定义的登录界面,那么就会重定向到我们自定义的页面,否则就会重定向到默认的登录页面。
然后,updateAuthenticationDefaults()方法又通过调用failureUrl()方法进行配置,这个时候实现了failureUrl 和failureHandler 的初始化,其中failureHandler 处理器实际上就是使用的SimpleUrlAuthenticationFailureHandler对象,实现如下:
//AbstractAuthenticationFilterConfigurer.java
public final T failureUrl(String authenticationFailureUrl) {
T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(
authenticationFailureUrl));
this.failureUrl = authenticationFailureUrl;
return result;
}
public final T failureHandler(
AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return getSelf();
}
而AbstractAuthenticationProcessingFilter对象(实际是UsernamePasswordAuthenticationFilter对象)初始化的时候,又调用AbstractAuthenticationFilterConfigurer类的configure()方法,进而调用了AbstractAuthenticationProcessingFilter对象的setAuthenticationFailureHandler()方法,把上一步中初始化的SimpleUrlAuthenticationFailureHandler对象赋值给了AbstractAuthenticationProcessingFilter对象的failureHandler 变量,实现如下:
//AbstractAuthenticationFilterConfigurer.java
@Override
public void configure(B http) throws Exception {
authFilter.setAuthenticationFailureHandler(failureHandler);
}
//AbstractAuthenticationProcessingFilter.java
public void setAuthenticationFailureHandler(
AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
前面提到的内容,其实都是在启动项目时,进行初始化的。那么初始化之后,又是如何产生作用的呢?我们下面开始分析认证失败处理器是如何工作的。
认证失败处理器其实就是在unsuccessfulAuthentication()方法中调用执行的,实现如下:
//AbstractAuthenticationProcessingFilter.java
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
//省略 debug ……
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
在unsuccessfulAuthentication()方法中,首先清除SecurityContextHolder中存储的上下文信息,然后通过rememberMeServices的loginFail()方法处理浏览器缓存信息,最后通过调用failureHandler的onAuthenticationFailure()方法完成认证失败处理器的调用,这里failureHandler对象其实就是SimpleUrlAuthenticationFailureHandler对象。
首先,我们分析一下rememberMeServices的loginFail()方法,该方法就是处理浏览器缓存的,实现如下:
//AbstractRememberMeServices.java
@Override
public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
logger.debug("Interactive login attempt was unsuccessful.");
cancelCookie(request, response);
onLoginFail(request, response);
}
//空方法,不做任何实现
protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {
}
//处理浏览器cookie信息
protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) {
logger.debug("Cancelling cookie");
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
cookie.setPath(getCookiePath(request));
if (cookieDomain != null) {
cookie.setDomain(cookieDomain);
}
if (useSecureCookie == null) {
cookie.setSecure(request.isSecure());
}
else {
cookie.setSecure(useSecureCookie);
}
response.addCookie(cookie);
}
然后,我们分析一下SimpleUrlAuthenticationFailureHandler的onAuthenticationFailure()方法,该方法主要实现认证失败的后续处理,一般是实现页面的跳转或认证失败数据的返回,前面已经分析过该方法,这里不再贴出代码了。
至此基于SpringSecurity的认证失败处理流程,我们基本上就学习完了,下一节我们将继续学习其他的内容。