Spring Security 自定义表单登录

1,583 阅读8分钟

日积月累,水滴石穿 😄

前言

本文内容跟着上文继续,Spring Security 的入门案例。 到目前为止我们的SecurityConfig只包含了关于如何验证用户的信息。那 Security怎么知道我们需要对所有的接口进行验证?Security又是怎么知道我们需要支持基于表单的验证?原因是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了一个默认的配置,代码如下:

protected void configure(HttpSecurity http) throws Exception {
    this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
    ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
}
  • anyRequest().authenticated():请求都需要被认证
  • formLogin:基于表单认证
  • httpBasic:可以配置basic登录

自定义表单登录

默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成。在 Spring Security 中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是 /login,也就是说,默认会存在如下两个请求:

如果发起的是 GET 请求表示你想访问登录页面,如果发起的是 POST 请求,表示你想提交登录数据。而且默认的请求参数为 username、password。那这些是在哪里规定的呢?当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器。

image.png

  • usernamePasrameter :账户参数名
  • passwordParameter :密码参数名
  • postOnly=true :默认情况下只允许POST请求

但是大多数应用程序都有自己的登录页面,登录接口,接口请求参数名称。 比如不想使用 Security 提供的登录页面,不想调用 login 接口,想改名为 /auth/login,表单参数不想使用 username、password,而是想使用 account、pwd,要做到这一点,只需要配置如下代码:

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()  //开启配置
            .anyRequest() //其他请求
            .authenticated()//验证   表示其他请求需要登录才能访问
            .and()
            // 增加如下
            .formLogin()
            .loginPage("/login.html") //登录页面
            .loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
            .usernameParameter("account") //用户名字段
            .passwordParameter("pwd") //密码字段
            .defaultSuccessUrl("/hello")  //登录成功的回调
            .permitAll() // 上述 login.html 页面、/auth/login接口放行
            .and()
            .csrf().disable();  // 禁用 csrf 保护
}

html页面内容如下,并将其配置在 resources/static 目录下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<div id="login">
    <h1>Login</h1>
    <form method="post" action="/auth/login">
        <input type="text" required="required" placeholder="用户名" name="account"></input>
        <input type="password" required="required" placeholder="密码" name="pwd"></input>
        <button class="but" type="submit">登录</button>
    </form>
</div>
</body>
</html>  

配置完成之后,重启项目。请求 /hello 接口,跳转到指定的页面,如下:

image.png

登录回调

登录回调分为两种情况:

  • 前后端不分离的登录回调
  • 前后端分离的登录回调

两种情况的处理方式不一样。我们先来看第一种前后端不分的登录回调。

前后端不分离的登录回调

登录成功重定向

登录成功重定向 URL 相关的方法有两个:

  • successForwardUrl:表示不管你在浏览器输入什么地址,登录后一律跳转到 successForwardUrl 指定的地址。
  • defaultSuccessUrl:指定登录成功的跳转页面或者接口。比如指定为 /hello,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /hello,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/cxyxj/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /hello ,而是来到 /cxyxj/hello 页面。如果不配置defaultSuccessUrl,直接在浏览器中输入登录\接口地址,登录成功后,就直接跳转到 /image.png defaultSuccessUrl还有一个重载方法,第二个参数默认为 false,与 defaultSuccessUrl方法一致,如果设置为 true,效果与successForwardUrl方法一致。

登录失败重定向

  • failureForwardUrl
  • failureUrl

failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,发生重定向。

在实际开发中(虽然上述方式用的少),不论是登录成功回调还是登录失败回调只需要选择一种方式配置即可否则会发生冲突。如下配置:

http.authorizeRequests()  //开启配置
        .antMatchers("/hello2","/hello4").permitAll()
        .anyRequest() //其他请求
        .authenticated()//验证   表示其他请求需要登录才能访问
        .and()
        // 增加如下
        .formLogin()
        .loginPage("/login.html") //登录页面
        .loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
        .usernameParameter("account") //用户名字段
        .passwordParameter("pwd") //密码字段
        .defaultSuccessUrl("/hello")  //登录成功的回调
        .successForwardUrl("/hello3")
        .failureUrl("/hello2")
        .failureForwardUrl("/hello4")
        .permitAll() // 上述 login.html 页面、/auth/login接口放行
        .and()
        .csrf().disable();  // 禁用 csrf 保护

image.png

前后端分离的登录回调

登录成功

需要使用到的方法是successHandler,用来处理登录成功的回调,可以自定义前后端交互的数据。该方法的入参为 AuthenticationSuccessHandler接口,重写其 onAuthenticationSuccess方法可以自定义登录的逻辑,onAuthenticationSuccess方法有三个入参:

void onAuthenticationSuccess(HttpServletRequest var1, 
HttpServletResponse var2, Authentication var3) 

前面两个参数就不介绍了。

  • Authentication:保存了登录后的用户信息

登录成功之后,返回 json 数据给前端,可以进行如下配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.authorizeRequests() //开启配置
    // /hello2、/hello4接口可以直接访问,不需要登录
    .antMatchers("/hello2", "/hello4").permitAll().anyRequest() //其他请求
    .authenticated() //验证   表示其他请求需要登录才能访问
    .and().formLogin().loginPage("/login.html") //登录页面
    .loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
    .usernameParameter("account") //用户名字段
    .passwordParameter("pwd") //密码字段
    // 增加如下
    .successHandler((request, response, authentication) - > {
      Object principal = authentication.getPrincipal();
      response.setContentType("application/json;charset=utf-8");
      PrintWriter out = response.getWriter();
      out.write(new ObjectMapper().writeValueAsString(principal));
      out.flush();
      out.close();
    }).permitAll() // 上述 login.html 页面、/auth/login接口放行
    .and().csrf().disable(); // 禁用 csrf 保护
}

配置完成后,项目重新启动,这里使用请求工具进行请求,给各位小伙伴看的更为直观。可以看到登录成功的用户信息通过 JSON 响应,如下: image.png 返回的密码被置为了空,角色带上了前缀 ROLE_。 如果你对代码有洁癖,觉得上述写法有难看,也也可以实现AuthenticationSuccessHandler接口,自己重写 onAuthenticationSuccess方法,

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Object principal = authentication.getPrincipal();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(principal));
        out.flush();
        out.close();
    }
}

然后将自定义的类配置进去:successHandler(new MyAuthenticationSuccessHandler())

登录失败

与之对应的,登录失败也有类似的回调,方法为;failureHandler,入参为 AuthenticationFailureHandler接口。重写其 onAuthenticationFailure方法可以自定义登录的逻辑,onAuthenticationFailure方法有三个入参:

void onAuthenticationFailure(HttpServletRequest var1, 
HttpServletResponse var2, AuthenticationException var3)

前面两个参数就不介绍了。

  • AuthenticationException:登录失败的原因,可以根据不同的异常类型,响应用户更加明确的提示信息。
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        String msg = "";
        if (e instanceof LockedException) {
            msg = "账户被锁定,请联系管理员!";
        }
       else if (e instanceof BadCredentialsException) {
            msg = "用户名或者密码输入错误,请重新输入!";
        }
        out.write(msg);
        out.flush();
        out.close();
    }
}

添加自定义配置:.failureHandler(new MyAuthenticationFailureHandler())。重新启动项目,进行测试,故意输错密码。

image.png

常见的异常类型
  • LockedException:账户被锁定
  • CredentialsExpiredException:密码过期
  • AccountExpiredException:账户过期
  • DisabledException:账户被禁用
  • BadCredentialsException:用户名或者密码错误

未认证处理

如果没有登录就访问需要认证的数据,security 默认情况下,会重定向到登录页面。 但是在实际的项目中,有时候不需要这样。我们希望给用户一个尚未登录的提示,然后前端收到提示之后,再决定干什么。要达到这个效果,我们可以使用AuthenticationEntryPoint这个接口进行自定义,取消默认的重定向行为。 该接口中有一个方法 commence,方法有三个参数:

void commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)
  • AuthenticationException:未认证的exception。
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("您未登录,请先登录!");
        out.flush();
        out.close();
    }
}

在配置csrf().disable()之后添加 .exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint()); 配置完成之后,请求 hello接口,响应结果如下:

image.png

注销登录

有登录功能,那肯定还有注销功能。在 Security中默认的接口就是 logout,是一个 GET 请求。 成功退出后跳转到 /login?logout。也是可以进行配置的。

前后端不分离

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()  //开启配置
            .antMatchers("/cxyxj/**").hasRole("admin")   //访问/cxyxj/**下的路径,必须具备admin身份
            .antMatchers("/security/**").hasRole("user") //访问/security/**下的路径,必须具备user身份
            .antMatchers("/permitAll").permitAll()  // 访问/permitAll路径,不需要登录
            .anyRequest() //其他请求
            .authenticated()//验证   表示其他请求只需要登录就能访问
            .and()
            .formLogin()
            .loginPage("/login.html") //登录页面
            .loginProcessingUrl("/auth/login") //登录接口
            .usernameParameter("account") //用户名字段
            .passwordParameter("pwd") //密码字段
            .defaultSuccessUrl("/hello")  //登录成功的回调
            .permitAll() // 上述 formLogin() 的页面、接口放行
            .and()
            // 增加如下
            .logout()  //开启注销登陆
            .logoutUrl("/auth/logout") //注销请求url
            .logoutSuccessUrl("/login.html") //注销成功后要跳转的页面
            .deleteCookies()  // 清除 cookie
            .clearAuthentication(true) //清除认证信息
            .invalidateHttpSession(true) //使session失效
            .permitAll()
            .and()
            .csrf().disable(); // 禁用 csrf 保护

}
  • logoutUr:注销登录的URL。
  • logoutSuccessUrl:表示注销成功后要跳转的页面。
  • deleteCookies:清除 cookie。
  • clearAuthentication:清除认证信息,默认为 true。
  • invalidateHttpSession:使 HttpSession 失效,默认为 true。

前后端分离

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("注销成功");
        out.flush();
        out.close();
    }
}

配置如下:

.and()
.logout()
.logoutUrl("/auth/logout") //注销请求url
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.permitAll()

image.png


  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。