认证失败
继上一篇内容我们继续讨论一下关于表单登录配置认证失败的时候,它默认页面的跳转原理。
前言
为了方便在前端页面中展示失败的异常信息,我们现在项目中的pom.xml文件中引入thymeleaf依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
接下来修改一下上篇文章中的页面,增加一个div用来展示异常信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qcfkpmio-1646990447899)(C:\Users\chenx\Desktop\新建文件夹\SpringSecurity\i异常信息-login.png)]
既然现在的页面是动态页面,那么就不能像静态页面那样直接访问,需要我们提供一个访问控制器。
@GetMapping({"/","login",""})
public String login(){
return "login";
}
最后在SecurityConfig中配置页面登录。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successHandler(new MyAuthenticationSuccessHandler())
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
failureUrl表示登录失败后重定向到login.html页面,重定向是一种客户端跳转,重定向不方便携带请求失败的异常信息(只能放在URL中)
如果希望能够在前端展示请求失败的异常信息,可以使用下面这种方式:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successHandler(new MyAuthenticationSuccessHandler())
// .failureUrl("/login.html")
.failureForwardUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
failureForwardUrl方法从名字上就可以看出,这种跳转是一个服务器端跳转,服务器端跳转的好处是可以携带登录异常信息。如果登录失败,自动跳转到登录页面后,就可以将错误信息展示出来。
无论是failureUrl还是failureForwardUrl,最终所配置的都是AuthenticationFailureHandler接口实现的,SpringSecurity中提供了AuthenticationFailureHandler接口,用来规范登录失败的实现:
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException;
}
AuthenticationFailureHandler中只有一个方法,用来处理登录失败请求,最后的exception参数则表示登录失败的异常信息。SpringSecurity中为AuthenticationFailureHandler一共提供了五个实现类,如下:
(1)、SimpleUrlAuthenticationFailureHandler 默认的处理逻辑就算通过重定向跳转到登录页面,当然也可以通过配置forwardToDestination属性将重定向改为服务器端跳转,failureUrl方法底层的实现逻辑就是SimpleUrlAuthenticationFailureHandler
(2)、ExceptionMappingAuthenticationFailureHandler 可以实现根据不同的异常类型,映射到不同的路径。
(3)、ForwardAuthenticationFailureHandler 表示通过服务器端跳转来重新回到登录页面,failureForwardUrl方法底层实现逻辑就是ForwardAuthenticationFailureHandler
(4)、AuthenticationEntryPointFailureHandler 是SpringSecurity 5.2新引进的处理类,可以通过AuthenticationEntryPoint来处理登录异常。
(5)、DelegatingAuthenticationFailureHandler 可以实现为不同的异常类型配置不同的登录失败处理回调。
这里举一个简单的例子,如果不适用failureForwardUrl方法,同时又想在登录失败后通过服务器端跳转回到登录页面,那么可以自定义SimpleUrlAuthenticationFailureHandler配置,并将forwardToDestination属性设置为true,代码如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(failureHandler())
// .failureUrl("/login.html")
// .failureForwardUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
SimpleUrlAuthenticationFailureHandler failureHandler(){
SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
handler.setDefaultFailureUrl("/login.html");
handler.setUseForward(true);
return handler;
}
}
这样配置后,如果用户在此登录失败,就会通过服务端跳转重新回到登录页面,登录页面也会展示相应的错误信息,效果和failureForwardUrl一致。
SimpleUrlAuthenticationFailureHandler 的源码也很简单,我们也来看看(列出核心部分)
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
setDefaultFailureUrl(defaultFailureUrl);
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
}
else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
}
else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
if (this.forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
return;
}
HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
}
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
}
从这段源码中可以看到,当用户构造SimpleUrlAuthenticationFailureHandler对象的时候,就传入了defaultFailureUrl,也就是登录失败时要跳转的地址。在onAuthenticationFailure方法中,如果发现了defaultFailureUrl为null,则直接通过response返回异常信息,否则调用saveException方法,在saveException方法中,如果forwardToDestination为true,就通过服务区端跳转回到登录页面,否则通过重定向回到登录页面。
如果是前后的分离开发,登录失败就不需要页面跳转了,只需要返回JSON字符串给前端即可,此时可以通过自定义AuthenticationFailureHandler的实现类来完成,如下:
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
HashMap<String,Object> resp = new HashMap<>();
resp.put("status",200);
resp.put("msg",exception.getMessage());
ObjectMapper om = new ObjectMapper();
final String writeValueAsString = om.writeValueAsString(resp);
response.getWriter().write(writeValueAsString);
}
}
同样在WebSecurity中配置
http.failureHandler(new MyAuthenticationFailureHandler());
这样在登录失败后,就不会进行页面跳转了,而是直接返回JSON字符串。