# 自定义表单认证和配置

# 1. 自定义表单

  • 配置『自定义表单认证』核心代码段

    http.formLogin()
        .loginPage("...")
        .loginProcessingUrl("...")
        ...;
    
  • 准备自定义登录页面(可以是一个纯 html 页面)

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8"/>
      <title>登录页</title>
    </head>
    <body>
      <form action="/login.do" method="post">
        <p><input name="username" value="tommy" placeholder="username"></p>
        <p><input name="password" value="123" placeholder="password"></p>
        <p><button type="submit">登录</button></p>
      </form>
    </body>
    </html>
    
  • SpringSecurityConfig 类中的配置代码

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        http.formLogin()
            .loginPage("/sign-in.html")
            .loginProcessingUrl("/login.do")
            .permitAll(); // 1  这句配置很重要,新手容易忘记。放开 sign-in.html 的访问权
    
        http.authorizeRequests()
            .anyRequest()
            .authenticated(); // 2
    
        http.csrf().disable();// 3
    }
    

自定义表单认证的配置和 Spring Security 自带表单认证配置很像。不同点就在于明确指明了登陆页面,以及登陆页面上登陆表单的 action

不过,这里仍有几处小细节需要明确说明:

  • 在 Spring Security 自带的表单验证中,我们不需要指定 .permitAll,但是在自定义的表单验证中则需要。表示该页面和登陆请求是匿名可访问的(否则,逻辑上说不通)

  • 登陆页面的名字不强求必须是 sign-in.html,可以自定义。

  • .loginPage 方法的参数除了可以是一个静态的页面(例如,/sign-in.html之外,可以是一个非静态页面的通用的 URI(例如,/login-page.do

    Spring Security 会重定向到这个 URI,触发 Controller 的执行,由 Controller 的返回值再来决定显示哪个登录页面。

  • 登陆页面上的表单提交方式『必须』是 post 方式。

  • .loginProcessingUrl 方法的参数值具体是什么无所谓,但是要和登陆页面(sign-in.html)上的 <form action="..."> 值一致。

  • 如果登陆页面的 form 表单的 action 属性值是 /login,那么 .loginProcessingUrl() 可以省略,因为它的默认值就是 /login

    如果,你的 @RequestMapping 的值是 /login,由于在这种情况下,Spring MVC 默认会忽略掉后缀,因此,form 表单的 action="/login.do"action="/login.action"action="/login.xxx"action="/login" 等价。

代码配置的链式调用的连写:

http.formLogin()
    .loginPage("/sign-in.html")
    .loginProcessingUrl("/login.do")
    .permitAll();
http.authorizeRequests()
    .anyRequest().authenticated();
http.csrf()
    .disable();

# 2. 过滤器链中的 UsernamePasswordAuthenticationFilter

在 Spring Security 中 form 表单方式的登录处理是由过滤器链中的 UsernamePasswordAuthenticationFilter 处理的,即,你的登录请求在 Spring Security 的 Filter 链中『走到』UsernamePasswordAuthenticationFilter 时,会被它识别、处理。

UsernamePasswordAuthenticationFilter 会从请求中获取到用户名和密码,再和 UserDetailsService 所提供的标准答案匹对,最终给出『通过认证』或『无法通过认证』的答案。

在这个 UsernamePasswordAuthenticationFilter 中

  • 默认的登录请求 url 是 /login

  • 默认的两个请求参数分别是 username 和 password

  • 默认的请求方式是 post

要么你自定义的登录页面必须满足以上默认的条件,要么进行配置,手动指定。

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.formLogin()
      .loginPage("/sign-in.html")
      .loginProcessingUrl("/login");     // 相对于 context-path 的路径,即,不包含 context-path
      // .usernameParameter("username")
      // .passwordParameter("password")

  http.authorizeRequests()
      .antMatchers("/sign-in.html").permitAll()
      .anyRequest().authenticated();

  http.csrf()
      .disable();  //  关闭 csrf 功能
}

默认,Spring Security 开起了 CSRF Token 功能(跨站请求伪造攻击防护),因此,此时需要通过配置先关闭掉。

TIP

但是有时候(其实就是 RESTful 风格的 API)中,你需要的并不是页面跳转,而是服务端返回 JSON 格式的数据,其中包含登录成功或失败信息。这种情况下,需要使用别的方案处理(见后续内容)

# 3. 认证成功之后

# 3.1 默认行为

登录成功后的跳转页面、跳转路径有 2 种:

  1. 如果用户是直接请求登录页面,那么登录成功后,默认会跳转至当前应用的根路径(/)。

  2. 如果用户时访问某个受限页面/请求,被转到登录页面,那么登录成功后,默认会跳转至原本受限制的页面/请求。

当然,上述是『默认情况』,你可以通过配置,强行指定无论如何,在登录成功后,都跳转至 xxx 页面。

// 登录页面配置
http.formLogin()
    .defaultSuccessUrl("/success.jsp");
//  .defaultSuccessUrl("/success.jsp", true);

通过 .defaultSuccessUrl() 可以指定上述第 1 种情况下的成功跳转页面。如果多加一个参数 true,那么第 2 种情况下,登录成功后也会被强制跳转至这个特定页面。

类似的,通过 .failureForwardUrl() 可以指定登录失败时跳转的错误页面。

# 3.2 登录成功返回 JSON

在某些前后端完全分离,仅靠 JSON 完成所有交互的系统中,一般会在登陆成功时返回一段 JSON 数据,告知前端,登陆成功与否。

在这里,可以通过 .successHandler 方法和 .failureHandler 方法指定『认证通过』之后和『认证未通过』之后的处理逻辑。

http.formLogin()
    .loginPage("/sign-in.html")
    .loginProcessingUrl("/login.do")
    .successHandler(new SimpleAuthenticationSuccessHandler())
    .failureHandler(new SimpleAuthenticationFailureHandler())
    .permitAll();

上面的 SimpleAuthenticationSuccessHandlerSimpleAuthenticationFailureHandler 类分别是 AuthenticationSuccessHandlerAuthenticationFailureHandler 接口的实现类,它们负责实现具体的回复逻辑:

class SimpleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest req,
            HttpServletResponse resp,
            Authentication authentication) throws IOException, ServletException {
        // authentication 对象携带了当前登陆用户名等相关信息
        // User user = (User) authentication.getPrincipal();
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.write("JSON 格式字符串");
    }
}

class SimpleAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(
            HttpServletRequest req,
            HttpServletResponse resp,
            AuthenticationException e) throws IOException, ServletException {
        // e 对象携带了认证的错误原因
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.write("JSON 格式字符串");
    }
}

在 Spring Security 和 JWT 整合时,我们会用到上面的 AuthenticationSuccessHandler 机制。