3-SpringSecurity:自定义Form表单

简介: 3-SpringSecurity:自定义Form表单

背景


本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity的各项Feature。


新建一个SpringBoot项目,起名springboot-security-form,核心依赖为WebSpringSecurityThymeleaf

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>


实验0:HttpBasic


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // There is no PasswordEncoder mapped for the id "null"
    PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    String yourPassword = "123";
    System.out.println("Encoded password: " + encoder.encode(yourPassword));
    // Config account info and permissions
    auth.inMemoryAuthentication()
    .withUser("dev").password(encoder.encode(yourPassword)).authorities("p1")
    .and()
    .withUser("test").password(encoder.encode(yourPassword)).authorities("p2");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .httpBasic();
}

image.png


实验1:自定义登录页面


  • 登录页面配置
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login");
}
  • 后端登录页面接口
@Controller
public class LoginController {
  @GetMapping("/login")
  public String login() {
    return "login";
  }
  @GetMapping(value = "/user/add")
  @ResponseBody
  public String accessResource1() {
    return " Access Resource 1: Add User";
  }
  @GetMapping(value = "/user/query")
  @ResponseBody
  public String accessResource2() {
    return " Access Resource 2: Query User";
  }
}
  • 前端模板
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<form action="login" method="post">
    <span>用户名</span><input type="text" name="username" /> <br>
    <span>密码</span><input type="password" name="password" /> <br>
    <input type="submit" value="登录">
</form>
</body>
</html>

Note:此时,需要先关闭CSRF,.csrf().disable(),否则报403;


实验2:自定义登录接口


默认登录页面接口与登录数据提交接口是同一个:/login,顺着.loginPage,进入FormLoginConfigurer,源码如下:

@Override
public FormLoginConfigurer<H> loginPage(String loginPage) {
    return super.loginPage(loginPage);
}

继续进入父类的loginPage方法,

protected T loginPage(String loginPage) {
    setLoginPage(loginPage);
    updateAuthenticationDefaults();
    this.customLoginPage = true;
    return getSelf();
}

继续跟踪进入方法updateAuthenticationDefaults();,可以看到,如果没有配置loginProcessingUrl,那么loginProcessingUrlloginPage便相同。

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");
    }
}

下面我们自定义登录数据提交接口为/formLogin,此时相应的前端action也要修改。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .loginProcessingUrl("/formLogin");
}
<form action="formLogin" method="post">
    <span>用户名</span><input type="text" name="username" /> <br>
    <span>密码</span><input type="password" name="password" /> <br>
    <input type="submit" value="登录">
</form>


实验3:自定义登录数据参数


  • 前面我们自定义了登录页面,在form表单中设置用户名、密码分别为username, password,那为什么这样写呢,可以改成别的嘛?可以倒是可以,但是不能随便改;


  • 如果这里我们把username改为name,再次尝试登录,后端接口将报错:org.springframework.security.authentication.BadCredentialsException: Bad credentials。。可是实际项目中我们的用户名密码就是不叫这个名字呢?我们可以进行配置.usernameParameter("name")
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .loginProcessingUrl("/formLogin")
            .usernameParameter("name");
}
<form action="formLogin" method="post">
    <span>用户名</span><input type="text" name="name" /> <br>
    <span>密码</span><input type="password" name="password" /> <br>
    <input type="submit" value="登录">
</form>

默认的用户名、密码分别为username, password,我们看下SpringSecurity的源码:

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
    AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
  /**
   * Creates a new instance
   * @see HttpSecurity#formLogin()
   */
  public FormLoginConfigurer() {
    super(new UsernamePasswordAuthenticationFilter(), null);
    usernameParameter("username");
    passwordParameter("password");
  }
}


实验4:自定义登录失败、成功处理器


问题:就以上个实验3中的报错信息为例,或当用户名、密码输错后,如何在后台看到错误信息?

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .loginProcessingUrl("/formLogin")
            .usernameParameter("name")
            .failureHandler(new AuthenticationFailureHandler(){
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    exception.printStackTrace();
                    request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
                }
            });
}

常见的认证异常,这里可以看到AuthenticationException共有18个子类:

image.png

上述增加了在认证失败时的处理:输出错误信息。同理,如果想在登录成功时直接进行一些处理(eg: 数据初始化等),可以使用以下配置:

.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        System.out.println("Login Successfully~");
        // do something here: initial work or forward to different url regarding different roles
        ...
        request.getRequestDispatcher("").forward(request, response);
    }
})


实验5:自定义登录成功跳转页面


经历千难万险,终于要登录成功了。进来之后要跳转到哪里呢?看你喽~想跳哪里跳哪里。。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .loginProcessingUrl("/formLogin")
            .usernameParameter("name")
            .failureHandler(new AuthenticationFailureHandler(){
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    exception.printStackTrace();
                    request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
                }
            })
            .successForwardUrl("/ok"); // custom login success page, a POST request
}
@Controller
public class LoginController {
  ...
  @PostMapping(value = "/ok")
  @ResponseBody
  public String ok() {
    return "ok";
  }
}

通过.successForwardUrl("/ok")配置了登录成功之后要跳转的页面路径或接口,同时需要在后端新增/ok接口。


Note:


  • 注意这里successForwardUrl的接口必须为POST接口;


  • 除了.successForwardUrl("/ok");,还可以使用.defaultSuccessUrl("/ok");或者.defaultSuccessUrl("/ok", true); 第二个参数true表示不管是从哪个地址进来,登录后全部跳转到指定的地址,此时与successForwardUrl效果相同,默认为false


  • 当然,除了登录成功后的跳转,还有登录失败后的跳转:failureForwardUrl


实验6:自定义退出接口


默认的退出接口是/logout,可进行配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/user/add").hasAuthority("p1")
            .antMatchers("/user/query").hasAuthority("p2")
            .antMatchers("/user/**").authenticated()
            .anyRequest().permitAll() // Let other request pass
            .and()
            .csrf().disable() // turn off csrf, or will be 403 forbidden
            .formLogin() // Support form and HTTPBasic
            .loginPage("/login")
            .loginProcessingUrl("/formLogin")
            .usernameParameter("name")
            .failureHandler(new AuthenticationFailureHandler(){
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    exception.printStackTrace();
                    request.getRequestDispatcher(request.getRequestURL().toString()).forward(request, response);
                }
            })
            .successForwardUrl("/ok") // custom login success page, a POST request
            .and()
            .logout()
            .logoutUrl("/leave");            
}

上述配置将退出接口改为/leave。在默认的退出过程中,还做了诸如清除认证信息和使Session失效等工作:

public class SecurityContextLogoutHandler implements LogoutHandler {
  protected final Log logger = LogFactory.getLog(this.getClass());
  private boolean invalidateHttpSession = true;
  private boolean clearAuthentication = true;
  // ~ Methods
  // ==========================================
  /**
   * Requires the request to be passed in.
   *
   * @param request from which to obtain a HTTP session (cannot be null)
   * @param response not used (can be <code>null</code>)
   * @param authentication not used (can be <code>null</code>)
   */
  public void logout(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) {
    Assert.notNull(request, "HttpServletRequest required");
    if (invalidateHttpSession) {
      HttpSession session = request.getSession(false);
      if (session != null) {
        logger.debug("Invalidating session: " + session.getId());
        session.invalidate();
      }
    }
    if (clearAuthentication) {
      SecurityContext context = SecurityContextHolder.getContext();
      context.setAuthentication(null);
    }
    SecurityContextHolder.clearContext();
  }
}


目录
相关文章
|
2月前
|
前端开发 JavaScript UED
JFinal框架中防止表单重复提交的方法
每种方法都有其优势和局限性。Token机制能提供较为稳定可靠的保护,但增加了服务器端的处理负担。锁定机制在处理并发请求时更有效,但可能导致用户体验的下降。JavaScript客户端控制提供了良好的用户体验,但依赖于客户端的执行环境。在实际应用中,可以根据具体的场景和需求,选择最适合的方法,或者将多种方法组合使用,以达到最佳的效果。
26 10
|
6月前
【SpringSecurity 】SpringSecurity 自定义登录页面
【SpringSecurity 】SpringSecurity 自定义登录页面
105 0
|
自然语言处理 前端开发 Java
SpringMVC表单标签
SpringMVC表单标签
|
前端开发 JavaScript Java
SpringMVC--服务器表单校验
SpringMVC--服务器表单校验
66 0
|
Java
SpringMVC中使用form:form表单标签报500错误
SpringMVC中使用form:form表单标签报500错误
115 0
|
前端开发 Java 数据库连接
学习SpringMVC必知必会(7)~springmvc的数据校验、表单标签、文件上传和下载
学习SpringMVC必知必会(7)~springmvc的数据校验、表单标签、文件上传和下载
218 0
学习SpringMVC必知必会(7)~springmvc的数据校验、表单标签、文件上传和下载
|
数据采集 前端开发 Java
SpringMVC【参数绑定、数据回显、文件上传】
我们在Controller使用方法参数接收值,就是把web端的值给接收到Controller中处理,这个过程就叫做参数绑定…
155 0
SpringMVC【参数绑定、数据回显、文件上传】
|
前端开发 Java 网络架构
SpringMVC的form表单标签使用(八)上
SpringMVC的form表单标签使用(八)
268 0
SpringMVC的form表单标签使用(八)上
|
存储 前端开发 数据处理
SpringMVC的form表单标签使用(八)下
SpringMVC的form表单标签使用(八)
387 0
SpringMVC的form表单标签使用(八)下
|
存储 前端开发 数据库
SpringMVC的form表单标签使用(八)中
SpringMVC的form表单标签使用(八)
263 0
SpringMVC的form表单标签使用(八)中