Spring Security登录表单配置(3)

打印 上一主题 下一主题

主题 742|帖子 742|积分 2226

1. 登录表单配置


1.1 快速入门

  理解了入门案例之后,接下来我们再来看一下登录表单的详细配置,首先创建一个新的Spring Boot项目,引入Web和Spring Security依赖,代码如下:
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>org.springframework.boot</groupId>
  7.     <artifactId>spring-boot-starter-web</artifactId>
  8. </dependency>
复制代码
  项目创建好之后,为了方便测试,需要在application.yml中添加如下配置,将登录用户名和密码固定下来:
  1. spring:
  2.   security:
  3.     user:
  4.       name: buretuzi
  5.       password: 123456
复制代码
  接下来,我们在resources/static目录下创建一个login.html页而,这个是我们自定义的登录页面:
查看代码
  1.  <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>登录</title>
  6.     <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
  7.    
  8.    
  9. </head>
  10. <body>
  11.    
  12.         
  13.             
  14.                
  15.                     
  16.                         <form id="login-form"  action="/dologin" method="post">
  17.                             <h3 >登录</h3>
  18.                            
  19.                                 <label for="username" >用户名:</label><br>
  20.                                 <input type="text" name="uname" id="username" >
  21.                            
  22.                            
  23.                                 <label for="password" >密码:</label><br>
  24.                                 <input type="text" name="passwd" id="password" >
  25.                            
  26.                            
  27.                                 <input type="submit" name="submit"  value="登录">
  28.                            
  29.                         </form>
  30.                     
  31.                
  32.             
  33.         
  34.    
  35. </body>
  36. </html>
复制代码
  这个logmt.html中的核心内容就是一个登录表单,登录表单中有三个需要注意的地方,

  • form的action,这里给的是/doLogin,表示表单要提交到/doLogin接口上。
  • 用户名输入框的name属性值为uname,当然这个值是可以自定义的,这里采用了uname。


  • 密码输入框的name属性值为passwd, passwd也是可以自定义的。
login.html定义好之后,接下来定义两个测试接口,作为受保护的资源。当用户登录成功 后,就可以访问到受保护的资源。接口定义如下:
  1. package com.intehel.demo.controller;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. @RestController
  5. public class LoginController {
  6.     @RequestMapping("/index")
  7.     public String index(){
  8.         return "login";
  9.     }
  10.     @RequestMapping("/hello")
  11.     public String hello(){
  12.         return "hello";
  13.     }
  14. }
复制代码
  最后再提供一个Spring Security的配置类:
  1. package com.intehel.demo.config;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  4. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  5. @Configuration
  6. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  7.     @Override
  8.     protected void configure(HttpSecurity http) throws Exception {
  9.         http.authorizeRequests()
  10.                 .anyRequest().authenticated()
  11.                 .and()
  12.                 .formLogin()
  13.                 .loginPage("/loginNew.html")
  14.                 .loginProcessingUrl("/doLogin")
  15.                 .defaultSuccessUrl("/index")
  16.                 .failureUrl("/loginNew.html")
  17.                 .usernameParameter("uname")
  18.                 .passwordParameter("passwd")
  19.                 .permitAll()
  20.                 .and()
  21.                 .csrf().disable();
  22.     }
  23. }
复制代码
  在Spring Security中,如果我们需要自定义配置,基本上都是继承自WebSecurityConfigurerAdapter来实现的,当然WebSecurityConfigurerAdapter本身的配置还是比较复杂,同时也是比较丰富的,这里先不做过多的展开,仅就结合上面的代码来解释,在下节中将会对这里的配置再做更加详细的介绍。

  • 首先configure方法中是一个链式配置,当然也可以不用链式配置,每一个属性配置完毕后再从重新开始写起
  • authorizeRequests()方法表示开启权限配置(该方法的含义其实比较复杂,在后面还会再次介绍该方法),.anyRequest().authenticated()表示所有的请求都要认证之后才能访问.
  • 有的读者会对and()方法表示疑惑,and()方法会返回HttpSecurityBuilder对象的一个 子类(实际上就是HttpSecurity),所以and()方法相当于又回到HttpSecuiity实例,重新开启 新一轮的配置。如果觉得and()方法很难理解,也可以不用and()方法, 在.anyRequest().authenticated。配置完成后直接用分号(;)结束,然后通过http.formLogin()继续配置表单登录。
  • formLogin()表示开启表单登录配置,loginPage用来配置登录页面地址; loginProcessingUrl用来配置登录接口地址;defaultSuccessUrl表示登录成功后的跳转地址; failureUrl表示登录失败后的跳转地址;usernameParameter表示登录用户名的参数名称; passwordParameter表示登录密码的参数名称;permitAll表示跟登录相关的页面和接口不做拦截, 直接通过。需要注意的是,loginProcessingUrl、usernameParameter、passwordParameter 需要和login-html中登录表单的配置一致。
  • 最后的csrf().disable()表示禁用CSRF防御功能,Spring Security自带了 CSRF防御机制,但是我们这里为了测试方便,先将CSRF防御机制关闭,在后面将会详细介绍CSRF攻击与防御问题。
  配置完成后,启动Spring Boot项目,浏览器地址栏中输入http://localhost:8080/index,会自动跳转到http://localhost:8080/loginNew.html页面,如图2-5所示。输入用户名和密码进行登录(用 户名为buretuzi,密码为123456),登录成功之后,就可以访问到index页面了,如图2-6所示。
  
图2-5

图2-6
  经过上面的配置,我们已经成功自定义了一个登录页面出来,用户在登录成功之后,就可以访问受保护的资源了。
1.2 配置细节

  当然,前面的配置比较粗糙,这里还有一些配置的细节需要和读者分享一下。
  在前面的配置中,我们用defaultSuccessUrl表示用户登录成功后的跳转地址,用failureUrl 表示用户登录失败后的跳转地址。关于登录成功和登录失败,除了这两个方法可以配置之外, 还有另外两个方法也可以配置。
  1.2.1 登录成功

  当用户登录成功之后,除了 defaultSuccessUrl方法可以实现登录成功后的跳转之外, successForwardUrl也可以实现登录成功后的跳转,代码如下:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/loginNew.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .successForwardUrl("/index")
  12.                 .failureUrl("/loginNew.html")
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .csrf().disable();
  18.     }
  19. }
复制代码
defaultSuccessUrl 和 successForwardUrl 的区别如下:

  • defaultSuccessUrl表示当用户登录成功之后,会自动重定向到登录之前的地址上, 如果用户本身就是直接访问的登录页面,则登录成功后就会重定向到defaultSuccessUrl指定的页面中。例如,用户在未认证的情况下,访问了/hello页面,此时会自动重定向到登录页面, 当用户登录成功后,就会自动重定向到/hello页面;而用户如果一开始就访问登录页面,则登录成功后就会自动重定向到defaultSuccessUrl所指定的页面中,
  • successForwardUrl则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到successForwardUrl所指定的页面
  • defaultSuccessUrl有一个重载方法,如果重载方法的第二个参数传入true,则 defaultSuccessUrl的效果与successForwardUrl类似,即不考虑用户之前的访问地址,只要登录成功,就重定向到defaultSuccessUrl所指定的页面。不同之处在于,defaultSuccessUrl是通过重定向实现的跳转(客户端跳转),successForwardUrl则是通过服务器端跳转实现的。
无论是 defaultSuccessUrl 还是 successForwardUrl,最终所配置的都是 AuthenticationSuccessHandler接口的实例。
Spring Security中专门提供了 AuthenticationSuccessHandler接口用来处理登录成功事项:
  1. public interface AuthenticationSuccessHandler {
  2.         default void onAuthenticationSuccess(HttpServletRequest request,
  3.                         HttpServletResponse response, FilterChain chain, Authentication authentication)
  4.                         throws IOException, ServletException{
  5.                 onAuthenticationSuccess(request, response, authentication);
  6.                 chain.doFilter(request, response);
  7.         }
  8.         void onAuthenticationSuccess(HttpServletRequest request,
  9.                         HttpServletResponse response, Authentication authentication)
  10.                         throws IOException, ServletException;
  11. }
复制代码
  由上述代码可以看到,AuthenticationSuccessHandler接口中一共定义了两个方法,其中一 个是default方法,此方法是Spring Security 5.2开始加入进来的,在处理特定的认证请求 AuthenticationFilter中会用到;另外一个非default方法,则用来处理登录成功的具体事项,其 中request和response参数好理解,authentication参数保存了登录成功的用户信息。我们将在后面的章节中详细介绍authentication参数。
  AuthenticationSuccessHandler接口共有三个实现类,如图2-7所示。
  
图2-7
(1) SimpleUrlAuthenticationSuccessHandler继承自 AbstractAuthenticationTargetUrlRequestHandler,通过 AbstractAuthenticationTargetUrlRequestHandler 中的 handle 方法实现请求重定向。
 (2)SavedRequestAwareAuthenticationSuccessHandler在 SimpleUrlAuthenticationSuccessHandler的基础上增加了请求缓存的功能,可以记录之前请求的地址,进而在登录成功后重定向到一开始访问的地址。
 (3) ForwardAuthenticationSuccessHandler的实现则比较容易,就是一个服务端跳转。
  我们来重点分析 SavedRequestAwareAuthenticationSuccessHandler和ForwardAuthenticationSuccessHandler的实现。
  当通过defaultSuccessUrl来设置登录成功后重定向的地址时,实际上对应的实现类就是 SavedRequestAwareAuthenticationSuccessHandler。
  1. public class SavedRequestAwareAuthenticationSuccessHandler extends
  2.                 SimpleUrlAuthenticationSuccessHandler {
  3.         protected final Log logger = LogFactory.getLog(this.getClass());
  4.         private RequestCache requestCache = new HttpSessionRequestCache();
  5.         @Override
  6.         public void onAuthenticationSuccess(HttpServletRequest request,
  7.                         HttpServletResponse response, Authentication authentication)
  8.                         throws ServletException, IOException {
  9.                 SavedRequest savedRequest = requestCache.getRequest(request, response);
  10.                 if (savedRequest == null) {
  11.                         super.onAuthenticationSuccess(request, response, authentication);
  12.                         return;
  13.                 }
  14.                 String targetUrlParameter = getTargetUrlParameter();
  15.                 if (isAlwaysUseDefaultTargetUrl()
  16.                                 || (targetUrlParameter != null && StringUtils.hasText(request
  17.                                                 .getParameter(targetUrlParameter)))) {
  18.                         requestCache.removeRequest(request, response);
  19.                         super.onAuthenticationSuccess(request, response, authentication);
  20.                         return;
  21.                 }
  22.                 clearAuthenticationAttributes(request);
  23.                 // Use the DefaultSavedRequest URL
  24.                 String targetUrl = savedRequest.getRedirectUrl();
  25.                 logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
  26.                 getRedirectStrategy().sendRedirect(request, response, targetUrl);
  27.         }
  28.         public void setRequestCache(RequestCache requestCache) {
  29.                 this.requestCache = requestCache;
  30.         }
  31. }
复制代码
  
这里的核心方法就是onAuthenticationSuccess:

  • 首先从requestcache中获取缓存下来的请求,如果没有获取到缓存请求,就说明用户在访问登录页面之前并没有访问其他页面,此时直接调用父类的onAuthenticationSuccess方法来处理最终会重定向到defaultSuccessUrl指定的地址。
  • 接下来会获取一个targetUrlParameter,这个是用户显式指定的、希望登录成功后重定向的地址,例如用户发送的登录请求是http://localhost:8080/doLogin?target=/hello,这就表示当用户登录成功之后,希望自动重定向到/hello这个接口,getTargetUrlParameter就是要获取重定向地址参数的key,也就是上面的target,拿到target之后,就可以获取到重定向地址了。
  • 如果 targetUrlParameter 存在,或者用户设置了 alwaysUseDefaultTargetUrl 为 true, 这个时候缓存下来的请求就没有意义了。此时会直接调用父类的onAuthenticationSuccess方法完成重定向口 targetUrlParameter存在,则直接重定向到targetUrlParameter指定的地址;alwaysUseDefaultTargetUrl 为 true,则直接重定向到 defaultSuccessUrl 指定的地址;如果 targetUrlParameter 存在并且 alwaysUseDefaultTargetUrl 为 true,则重定向到 defaultSuccessUrl 指定的地址。
  • 如果前面的条件都不满足,那么最终会从缓存请求savedRequest中获取重定向地址, 然后进行重定向操作。
  这就是SavedRequestAwareAuthenticationSuccessHandler的实现逻辑,升发者也可以配置 自己的 SavedRequestAwareAuthenticationSuccessHandler,代码如下:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/loginNew.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .successForwardUrl("/index")
  12.                 .failureUrl("/loginNew.html")
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .csrf().disable();
  18.     }
  19.     SavedRequestAwareAuthenticationSuccessHandler successHandler(){
  20.         SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
  21.         handler.setDefaultTargetUrl("/index");
  22.         handler.setTargetUrlParameter("target");
  23.         return handler;
  24.     }
  25. }
复制代码
  注意在配置时指定了 targetUrlParameter为target,这样用户就可以在登录请求中,通过 target来指定跳转地址了,然后我们修改一下前面login.html中的form表单:
  1. <form id="login-form"  action="/doLogin?target=/hello" method="post">
  2.     <h3 >登录</h3>
  3.    
  4.         <label for="username" >用户名:</label><br>
  5.         <input type="text" name="uname" id="username" >
  6.    
  7.    
  8.         <label for="password" >密码:</label><br>
  9.         <input type="text" name="passwd" id="password" >
  10.    
  11.    
  12.         <input type="submit" name="submit"  value="登录">
  13.    
  14. </form>
复制代码
  在form表单中,action修改/doLogin?target=/hello,这样当用户登录成功之后,就始终跳转到/hello接口了。
  当我们通过successForwardUrl来设置登录成功后重定向的地址时,实际上对应的实现类 就是 ForwardAuthenticationSuccessHandler,ForwardAuthenticationSuccessHandler 的源码特别简单,就是一个服务端转发,代码如下:
  1. public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  2.         private final String forwardUrl;
  3.         public ForwardAuthenticationSuccessHandler(String forwardUrl) {
  4.                 Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl),
  5.                                 () -> "'" + forwardUrl + "' is not a valid forward URL");
  6.                 this.forwardUrl = forwardUrl;
  7.         }
  8.         public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  9.                 request.getRequestDispatcher(forwardUrl).forward(request, response);
  10.         }
  11. }
复制代码
  由上述代码可以看到,主要功能就是调用getRequestDispatcher方法进行服务端转发。 AuthenticationSuccessHandler默认的三个实现类,无论是哪一个,都是用来处理页面跳转的,有时候页面跳转并不能满足我们的需求,特别是现在流行的前后端分离开发中,用户登录成功后,就不再需要页面跳转了,只需要给前端返回一个JSON数据即可,告诉前端登录成功还是登录失败,前端收到消息之后自行处理。像这样的需求,我们可以通过自定义 AuthenticationSuccessHandler 的实现类来完成:
  1. package com.intehel.demo.handler;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import org.springframework.security.core.Authentication;
  4. import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
  5. import javax.servlet.ServletException;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;
  8. import java.io.IOException;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  12.     @Override
  13.     public void onAuthenticationSuccess(
  14.             HttpServletRequest request, HttpServletResponse response, Authentication authentication)
  15.             throws IOException, ServletException {
  16.         response.setContentType("application/json;charset=UTF-8");
  17.         Map<String,Object> resp = new HashMap<String,Object>();
  18.         resp.put("status",200);
  19.         resp.put("msg","登录成功");
  20.         ObjectMapper om = new ObjectMapper();
  21.         String s = om.writeValueAsString(resp);
  22.         response.getWriter().write(s);
  23.     }
  24. }
复制代码
  在自定义的 MyAuthenticationSuccessHandler中,重写 onAuthenticationSuccess方法,在该方法中,通过HttpServletResponse对象返回一段登录成功的JSON字符串给前端即可。最后, 在 SecurityConfig中配置自定义的 MyAuthenticationSuccessHandler,代码如下:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/loginNew.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .successHandler(new MyAuthenticationSuccessHandler())
  12.                 .failureUrl("/loginNew.html")
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .csrf().disable();
  18.     }
  19. }
复制代码
  配置完成后,重启项目,此时,当用户成功登录之后,就不会进行页面跳转了,而是返回一段JSON字符串。

  1.2.2 登录失败

  接下来看登录失败的处理逻辑。为了方便在前端页面展示登录失败的异常信息,我们首先在项目的pom.xml文件中引入thymeleaf依赖,代码如下:
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4.     <version>2.0.7.RELEASE</version>
  5. </dependency>
复制代码
  然后在resources/templates目录下新建mylogin.html,代码如下:
  
查看代码
  1.  <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>登录</title>
  6.     <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
  7.    
  8.    
  9. </head>
  10. <body>
  11.    
  12.         
  13.             
  14.                
  15.                     <form id="login-form"  action="/doLogin?target=/hello" method="post">
  16.                         <h3 >登录</h3>
  17.                         
  18.                         
  19.                             <label for="username" >用户名:</label><br>
  20.                             <input type="text" name="uname" id="username" >
  21.                         
  22.                         
  23.                             <label for="password" >密码:</label><br>
  24.                             <input type="text" name="passwd" id="password" >
  25.                         
  26.                         
  27.                             <input type="submit" name="submit"  value="登录">
  28.                         
  29.                     </form>
  30.                
  31.             
  32.         
  33.    
  34. </body>
  35. </html>
复制代码
  mylogin.html和前面的login.html基本类似,前面的login.html是静态页面,这里的 mylogin.html是thymeleaf模板页面,mylogin.html页面在form中多了一个div,用来展示登录失败时候的异常信息,登录失败的异常信息会放在request中返回到前端,开发者可以将其直接提取岀来展示。
  既然mylogm.html是动态页面,就不能像静态页面那样直接访问了,需要我们给mylogin.html页面提供一个访问控制器:
  1. package com.intehel.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. @Controller
  5. public class MyLoginController {
  6.     @RequestMapping("/mylogin.html")
  7.     public String myLogin(){
  8.         return "mylogin";
  9.     }
  10. }
复制代码
  最后再在SecurityConfig中配置登录页面,代码如下:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/mylogin.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .defaultSuccessUrl("/index.html")
  12.                 .failureUrl("/mylogin.html")
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .csrf().disable();
  18.     }
  19. }
复制代码
  failureUrl表示登录失败后重定向到mylogin.html页面。重定向是一种客户端跳转,重定向不方便携带请求失败的异常信息(只能放在URL中)。
  如果希望能够在前端展示请求失败的异常信息,可以使用下面这种方式:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/mylogin.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .defaultSuccessUrl("/index.html")
  12.                 .failureForwardUrl("/mylogin.html")
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .csrf().disable();
  18.     }
  19. }
复制代码
  failureForwardUrl方法从名字上就可以看出,这种跳转是一种服务器端跳转,服务器端跳转的好处是可以携带登录异常信息,如果登录失败,自动跳转回登录页面后,就可以将错误信息展示出来,如图2-8所示。
  
图 2-8
  无论是 failureUrl 还是 failureForwardUrl,最终所配置的都是 AuthenticationFailureHandler 接口的实现。Spring Security中提供了 AuthenticationFailureHandler 接口,用来规范登录失败的 实现:
  1. public interface AuthenticationFailureHandler {
  2.         void onAuthenticationFailure(HttpServletRequest request,
  3.                         HttpServletResponse response, AuthenticationException exception)
  4.                         throws IOException, ServletException;
  5. }
复制代码
  AuthenticationFailureHandler 接口中只有一个 onAuthenticationFailure 方法,用来处理登录 失败请求,request和response参数很好理解,最后的exception则表示登录失败的异常信息。 Spring Security 中为 AuthenticationFailureHandler 一共提供了五个实现类如图 2-9 所示


图2-9

  • SimpleUrlAuthenticationFailureHandler默认的处理逻辑就是通过重定向跳转到登录页 面,当然也可以通过配置forwardToDestination属性将重定向改为服务器端跳转,failureUrl方法的底层实现逻辑就是 SimpleUrlAuthenticationFailureHandler。
  • ExceptionMappingAuthenticationFailureHandler可以实现根据不同的异常类型,映射到不同的路径
  • ForwardAuthenticationFailureHandler表示通过服务器端跳转来重新回到登录页面, failureForwardUrl 方法的底层实现逻辑就是 ForwardAuthenticationFailureHandler。
  • AuthenticationEntryPointFailureHandler是 Spring Security 5.2 新引进的处理类,可以 通过AuthenticationEntryPoint来处理登录异常。
  • DelegatingAuthenticationFailureHandler可以实现为不同的异常类型配置不同的登录失败处理回调。
  这里举一个简单的例子。假如不使用failureForwardUrl 方法,同时又想在登录失败后通过服务器端跳转回到登录页面,那么可以自定义SimpleUrlAuthenticationFailureHandler配置,并将forwardToDestination属性设置为true,代码如下:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/mylogin.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .defaultSuccessUrl("/index.html")
  12.                 .failureHandler(failureHandler())
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .csrf().disable();
  18.     }
  19.     SimpleUrlAuthenticationFailureHandler failureHandler(){
  20.         SimpleUrlAuthenticationFailureHandler handler =
  21.                 new SimpleUrlAuthenticationFailureHandler("/mylogin.html");
  22.         handler.setUseForward(true);
  23.         return handler;
  24.     }
  25. }
复制代码
  这样配置之后,如果用户再次登录失败,就会通过服务端跳转重新回到登录页面,登录页而也会展示相应的错误信息,效果和failureForwardUrl 一致。
SimpleUrlAuthenticationFailureHandler的源码也很简单,我们一起来看一下实现逻辑(源码比较长,这里列出来核心部分):
  1. public class SimpleUrlAuthenticationFailureHandler implements
  2.                 AuthenticationFailureHandler {
  3.         protected final Log logger = LogFactory.getLog(getClass());
  4.         private String defaultFailureUrl;
  5.         private boolean forwardToDestination = false;
  6.         private boolean allowSessionCreation = true;
  7.         private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
  8.         public SimpleUrlAuthenticationFailureHandler() {
  9.         }
  10.         public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
  11.                 setDefaultFailureUrl(defaultFailureUrl);
  12.         }
  13.         public void onAuthenticationFailure(HttpServletRequest request,
  14.                         HttpServletResponse response, AuthenticationException exception)
  15.                         throws IOException, ServletException {
  16.                 if (defaultFailureUrl == null) {
  17.                         logger.debug("No failure URL set, sending 401 Unauthorized error");
  18.                         response.sendError(HttpStatus.UNAUTHORIZED.value(),
  19.                                 HttpStatus.UNAUTHORIZED.getReasonPhrase());
  20.                 }
  21.                 else {
  22.                         saveException(request, exception);
  23.                         if (forwardToDestination) {
  24.                                 logger.debug("Forwarding to " + defaultFailureUrl);
  25.                                 request.getRequestDispatcher(defaultFailureUrl)
  26.                                                 .forward(request, response);
  27.                         }
  28.                         else {
  29.                                 logger.debug("Redirecting to " + defaultFailureUrl);
  30.                                 redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
  31.                         }
  32.                 }
  33.         }
  34.         protected final void saveException(HttpServletRequest request,
  35.                         AuthenticationException exception) {
  36.                 if (forwardToDestination) {
  37.                         request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
  38.                 }
  39.                 else {
  40.                         HttpSession session = request.getSession(false);
  41.                         if (session != null || allowSessionCreation) {
  42.                                 request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
  43.                                                 exception);
  44.                         }
  45.                 }
  46.         }
  47.         public void setDefaultFailureUrl(String defaultFailureUrl) {
  48.                 Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl),
  49.                                 () -> "'" + defaultFailureUrl + "' is not a valid redirect URL");
  50.                 this.defaultFailureUrl = defaultFailureUrl;
  51.         }
  52.         protected boolean isUseForward() {
  53.                 return forwardToDestination;
  54.         }
  55.         public void setUseForward(boolean forwardToDestination) {
  56.                 this.forwardToDestination = forwardToDestination;
  57.         }
  58. }
复制代码
  从这段源码中可以看到,当用户构造SimpleUrlAuthenticationFailureHandler对象的时候, 就传入了 defaultFailureUrl也就是登录失败时要跳转的地址。在onAuthenticationFailure方法中,如果发现defaultFailureUrl为null,则直接通过response返回异常信息,否则调用 saveException 方法。在 saveException 方法中,如果 fowardToDestination 属性设置为ture,表示通过服务器端跳转回到登录页面,此时就把异常信息放到request中。再回到 onAuthenticationFailure方法中,如果用户设置fowardToDestination 为 true,就通过服务器 端跳转回到登录页面,否则通过重定向回到登录页面。
  如果是前后端分离开发,登录失败时就不需要页面跳转了,只需要返回JSON字符串给前端即可,此时可以通过自定义AuthenticationFailureHandler的实现类来完成,代码如下
  1. package com.intehel.demo.handler;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import org.springframework.security.core.AuthenticationException;
  4. import org.springframework.security.web.authentication.AuthenticationFailureHandler;
  5. import javax.servlet.ServletException;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;
  8. import java.io.IOException;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
  12.     @Override
  13.     public void onAuthenticationFailure(
  14.             HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
  15.             throws IOException, ServletException {
  16.         response.setContentType("application/json;charset=UTF-8");
  17.         Map<String,Object> resp = new HashMap<String,Object>();
  18.         resp.put("status",500);
  19.         resp.put("msg","登录失败"+exception.getMessage());
  20.         ObjectMapper om = new ObjectMapper();
  21.         String s = om.writeValueAsString(resp);
  22.         response.getWriter().write(s);
  23.     }
  24. }
复制代码
  然后在SecurityConfig中进行配置即可:
  1. package com.intehel.demo.config;
  2. import com.intehel.demo.handler.MyAuthenticationFailureHandler;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  5. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  6. @Configuration
  7. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  8.     @Override
  9.     protected void configure(HttpSecurity http) throws Exception {
  10.         http.authorizeRequests()
  11.                 .anyRequest().authenticated()
  12.                 .and()
  13.                 .formLogin()
  14.                 .loginPage("/mylogin.html")
  15.                 .loginProcessingUrl("/doLogin")
  16.                 .defaultSuccessUrl("/index.html")
  17.                 .failureHandler(new MyAuthenticationFailureHandler())
  18.                 .usernameParameter("uname")
  19.                 .passwordParameter("passwd")
  20.                 .permitAll()
  21.                 .and()
  22.                 .csrf().disable();
  23.     }
  24. }
复制代码
  配置完成后,当用户再次登录失败,就不会进行页而跳转了,而是直接返回JSON字符串, 如图2-10所示。

图 2-10
  1.2.3 注销登录

  Spring Security中提供了默认的注销页面,当然开发者也可以根据自己的需求对注销登录进行定制。
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/mylogin.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .defaultSuccessUrl("/index.html")
  12.                 .failureHandler(new MyAuthenticationFailureHandler())
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .logout()
  18.                 .logoutUrl("/logout")
  19.                 .invalidateHttpSession(true)
  20.                 .clearAuthentication(true)
  21.                 .logoutSuccessUrl("/mylogin.html")
  22.                 .and()
  23.                 .csrf().disable();
  24.     }
  25. }
复制代码

  • 通过.logout()方法开启注销登录配置。
  • logoutUrl指定了注销登录请求地址,默认是GET请求,路径为/logout。
  • invalidateHttpSession 表示是否使 session 失效,默认为 true。
  • clearAuthentication表示是否清除认证信息,默认为true。
  • logoutSuccessUrl表示注销登录后的跳转地址。
  配置完成后,再次启动项目登录成功后,在浏览器中输入http://localhost:8080/logout就可以发起注销登录请求了,注销成功后,会自动跳转到mylogin.html页面。
  如果项目有需要,开发者也可以配置多个注销登录的请求,同时还可以指定请求的方法。
  1. package com.intehel.demo.config;
  2. import com.intehel.demo.handler.MyAuthenticationFailureHandler;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  5. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  6. import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  7. import org.springframework.security.web.util.matcher.OrRequestMatcher;
  8. @Configuration
  9. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  10.     @Override
  11.     protected void configure(HttpSecurity http) throws Exception {
  12.         http.authorizeRequests()
  13.                 .anyRequest().authenticated()
  14.                 .and()
  15.                 .formLogin()
  16.                 .loginPage("/mylogin.html")
  17.                 .loginProcessingUrl("/doLogin")
  18.                 .defaultSuccessUrl("/index.html")
  19.                 .failureHandler(new MyAuthenticationFailureHandler())
  20.                 .usernameParameter("uname")
  21.                 .passwordParameter("passwd")
  22.                 .permitAll()
  23.                 .and()
  24.                 .logout()
  25.                 .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
  26.                         new AntPathRequestMatcher("/logout2","POST")))
  27.                 .invalidateHttpSession(true)
  28.                 .clearAuthentication(true)
  29.                 .logoutSuccessUrl("/mylogin.html")
  30.                 .and()
  31.                 .csrf().disable();
  32.     }
  33. }
复制代码
上面这个配置表示注销请求路径有两个:

  • 第一个是/logout1,请求方法是GET。
  • 第二个是/logout2,请求方法是POST。
使用任意一个请求都可以完成登录注销。
如果项目是前后端分离的架构,注销成功后就不需要页面跳转了,只需将注销成功的信息返回给前端即可,此时我们可以自定义返回内容:
  1. @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/mylogin.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .defaultSuccessUrl("/index.html")
  12.                 .failureHandler(new MyAuthenticationFailureHandler())
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .logout()
  18.                 .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
  19.                         new AntPathRequestMatcher("/logout2","POST")))
  20.                 .invalidateHttpSession(true)
  21.                 .clearAuthentication(true)
  22.                 .logoutSuccessHandler((req,resp,auth)->{
  23.                     resp.setContentType("application/json;charset=UTF-8");
  24.                     Map<String,Object> result = new HashMap<String,Object>();
  25.                     result.put("status",200);
  26.                     result.put("msg","注销成功!");
  27.                     ObjectMapper om = new ObjectMapper();
  28.                     String s = om.writeValueAsString(result);
  29.                     resp.getWriter().write(s);
  30.                 })
  31.                 .and()
  32.                 .csrf().disable();
  33.     }
  34. }
复制代码
  
  配置 logoutSuccessHandler 和 logoutSuccessUrl 类似于前面所介绍的 successHandler 和defaultSuccessUrl之间的关系,只是类不同而已,因此这里不再赘述,读者可以按照我们前面的分析思路自行分析。
  配置完成后,重启项目,登录成功后再去注销登录,无论是使用/logout1还是/logout2进行注销,只要注销成功后,就会返回一段JSON字符串。
  如果开发者希望为不同的注销地址返回不同的结果,也是可以的,配置如下:
查看代码
  1.  @Configuration
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(HttpSecurity http) throws Exception {
  5.         http.authorizeRequests()
  6.                 .anyRequest().authenticated()
  7.                 .and()
  8.                 .formLogin()
  9.                 .loginPage("/mylogin.html")
  10.                 .loginProcessingUrl("/doLogin")
  11.                 .defaultSuccessUrl("/index.html")
  12.                 .failureHandler(new MyAuthenticationFailureHandler())
  13.                 .usernameParameter("uname")
  14.                 .passwordParameter("passwd")
  15.                 .permitAll()
  16.                 .and()
  17.                 .logout()
  18.                 .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
  19.                         new AntPathRequestMatcher("/logout2","POST")))
  20.                 .invalidateHttpSession(true)
  21.                 .clearAuthentication(true)
  22.                 .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
  23.                     resp.setContentType("application/json;charset=UTF-8");
  24.                     Map<String,Object> result = new HashMap<String,Object>();
  25.                     result.put("status",200);
  26.                     result.put("msg","使用logout1注销成功!");
  27.                     ObjectMapper om = new ObjectMapper();
  28.                     String s = om.writeValueAsString(result);
  29.                     resp.getWriter().write(s);
  30.                 },new AntPathRequestMatcher("/logout1","GET"))
  31.                 .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
  32.                     resp.setContentType("application/json;charset=UTF-8");
  33.                     Map<String,Object> result = new HashMap<String,Object>();
  34.                     result.put("status",200);
  35.                     result.put("msg","使用logout2注销成功!");
  36.                     ObjectMapper om = new ObjectMapper();
  37.                     String s = om.writeValueAsString(result);
  38.                     resp.getWriter().write(s);
  39.                 },new AntPathRequestMatcher("/logout1","GET"))
  40.                 .and()
  41.                 .csrf().disable();
  42.     }
  43. }
复制代码
  通过defaultLogoutSuccessHandlerFor方法可以注册多个不同的注销成功回调函数,该方法第一个参数是注销成功回调,第二个参数则是具体的注销请求。当用户注销成功后,使用了哪个注销请求,就给出对应的响应信息。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

火影

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表