Spring Security - 表单登录、记住和注销

内容

  • 简介和概述
  • 入门(实用指南)

简介和概述

Spring Security 带有大量内置功能和工具,方便我们使用。 在此示例中,我们将讨论其中三个有趣且有用的功能 −

  • 表单登录
  • 记住用户
  • 退出

表单登录

基于表单的登录是 Spring Security 提供支持的一种用户名/密码认证形式。 这是通过 Html 表单提供的。

每当用户请求受保护的资源时,Spring Security 都会检查请求的身份验证。 如果请求未经过身份验证/授权,用户将被重定向到登录页面。登录页面必须由应用程序以某种方式呈现。 Spring Security 默认提供该登录表单。

此外,如果需要,必须明确提供任何其他配置,如下所示 −

实例


protected void configure(HttpSecurity http) throws Exception {
http 
   // ... 
   .formLogin(
      form -> form       .loginPage("/login") 
      .permitAll() 
   ); 
}

此代码需要一个 login.html 文件存在于模板文件夹中,该文件将在点击 /login 时返回。该 HTML 文件应包含登录表单。 此外,该请求应该是对 /login 的发布请求。参数名称应分别为用户名和密码的"username"和"password"。 除此之外,表单中还需要包含一个 CSRF Token。

一旦我们完成了代码练习,上面的代码片段就会更加清晰。

Remember Me

这种类型的身份验证需要将记住我的 cookie 发送到浏览器。 此 cookie 存储用户信息/身份验证主体,并存储在浏览器中。因此,网站可以在下次会话启动时记住用户的身份。Spring Security 已经为此操作准备了必要的实现。 一种使用散列来保护基于 cookie 的令牌的安全性,而另一种使用数据库或其他持久性存储机制来存储生成的令牌。

注销

默认 URL /logout 通过以下方式将用户注销 −

  • 使 HTTP 会话无效
  • 清理所有已配置的 RememberMe 身份验证
  • 清除 SecurityContextHolder
  • 重定向到 /login?logout

WebSecurityConfigurerAdapter 自动将注销功能应用于 Spring Boot 应用程序。

入门(实用指南) 像往常一样,我们将从 start.spring.io 开始。 这里我们选择一个maven项目。我们将项目命名为"formlogin"并选择所需的 Java 版本。 我为这个例子选择了 Java 8。 我们还继续添加以下依赖项 −

  • Spring Web
  • Spring Security
  • Thymeleaf
  • Spring Boot 开发工具
Spring Initializr

Thymeleaf 是 Java 的模板引擎。 它允许我们快速开发静态或动态网页以在浏览器中呈现。 它具有极强的可扩展性,允许我们详细定义和自定义模板的处理。 除此之外,我们可以通过点击这个 https://www.thymeleaf.org 了解更多关于 Thymeleaf 的信息。

让我们继续生成我们的项目并下载它。 然后,我们将其解压缩到我们选择的文件夹中,并使用任何 IDE 打开它。 我将使用 Spring Tools Suite 4。 它可从 https://spring.io/tools 网站免费下载,并针对 spring 应用程序进行了优化。

让我们看一下我们的 pom.xml 文件。 它应该看起来与此类似 −

实例


<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion> 
   <parent> 
      <groupId>org.springframework.boot</groupId> 
      <artifactId>spring-boot-starter-parent</artifactId> 
      <version>2.3.1.RELEASE</version> 
      <relativePath /> 
      <!-- lookup parent from repository --> 
   </parent> 
   <groupId>            com.spring.security</groupId> 
   <artifactId>formlogin</artifactId> 
   <version>0.0.1-SNAPSHOT</version> 
   <name>formlogin</name> 
   <description>Demo project for Spring Boot</description> 
      
   <properties> 
      <java.version>1.8</java.version> 
   </properties> 
      
   <dependencies> 
      <dependency> 
         <groupId>org.springframework.boot</groupId> 
         <artifactId>spring-boot-starter-security</artifactId>
      </dependency> 
   <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-devtools</artifactId> 
   </dependency> 
   <dependency> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-test</artifactId> 
   <scope>test</scope> 
   <exclusions> 
      <exclusion> 
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId> 
      </exclusion> 
   </exclusions> 
   </dependency> 
   <dependency> 
      <groupId>org.springframework.security</groupId> 
      <artifactId>spring-security-test</artifactId> 
      <scope>test</scope> 
   </dependency> 
   </dependencies> 

   <build> 
      <plugins> 
         <plugin> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-maven-plugin</artifactId> 
         </plugin> 
      </plugins> 
   </build>
</project>

让我们在默认包下的文件夹 /src/main/java 中创建一个包。 我们将把它命名为 config,就像我们将所有的配置类放在这里一样。因此,名称应该与此类似 - com.tutorial.spring.security.formlogin.config。

The Configuration Class

实例


package com.tutorial.spring.security.formlogin.config; 

import java.util.List; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 
import org.springframework.security.core.userdetails.User; 
import org.springframework.security.core.userdetails.UserDetails; 
import org.springframework.security.core.userdetails.UserDetailsService; 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 
import org.springframework.security.crypto.password.NoOpPasswordEncoder; 
import org.springframework.security.crypto.password.PasswordEncoder; 
import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 

import com.spring.security.formlogin.AuthFilter;
 
@Configuration 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
   
   @Bean 
   protected UserDetailsService userDetailsService() {
   UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); 
   UserDetails user = User.withUsername("abby") 
   .password(passwordEncoder().encode("12345")) 
      .authorities("read") .build(); 
      userDetailsManager.createUser(user); 
      return userDetailsManager; 
      
   }
   @Bean 
   public PasswordEncoder passwordEncoder() { 
      return new BCryptPasswordEncoder(); }; 
      @Override 
      protected void configure(HttpSecurity http) throws Exception { 
      http.csrf().disable() .authorizeRequests().anyRequest()
      .authenticated() .and() 
      .formLogin() 
      .and() 
      .rememberMe() 
      .and() .logout() .logoutUrl("/logout") 
      .logoutSuccessUrl("/login") .deleteCookies("remember-me"); 
   } 
}

代码分解

在我们的配置包中,我们创建了 WebSecurityConfig 类。 这个类扩展了 Spring Security 的 WebSecurityConfigurerAdapter。我们将把这个类用于我们的安全配置,所以让我们用一个@Configuration 注解来注解它。 因此,Spring Security 知道将此类视为配置类。正如我们所看到的,Spring 使配置应用程序变得非常容易。

让我们看一下我们的配置类。

  • 首先,我们将使用 userDetailsService() 方法创建 UserDetailsService 类的 bean。 我们将使用这个 bean 来管理这个应用程序的用户。在这里,为了简单起见,我们将使用 InMemoryUserDetailsManager 实例来创建用户。 这个用户,连同我们给定的用户名和密码,将包含一个简单的"read"读取权限。
  • 现在,让我们看看我们的 PasswordEncoder。 对于这个例子,我们将使用 BCryptPasswordEncoder 实例。 因此,在创建用户时,我们使用 passwordEncoder 对我们的明文密码进行编码,如下所示
.password(passwordEncoder().encode("12345"))
  • 完成上述步骤后,我们继续进行下一个配置。 在这里,我们重写了 WebSecurityConfigurerAdapter 类的配置方法。此方法以 HttpSecurity 作为参数。 我们将配置它以使用我们的表单登录和注销,以及记住我的功能。

Http 安全配置

我们可以观察到所有这些功能都在 Spring Security 中可用。 让我们详细研究以下部分 −

实例


http.csrf().disable()         
   .authorizeRequests().anyRequest().authenticated() 
   .and() 
   .formLogin() 
   .and() 
   .rememberMe() 
   .and() 
   .logout()
   .logoutUrl("/logout") .logoutSuccessUrl("/login") .deleteCookies("remember-me");

这里有几点需要注意 −

  • 我们已禁用 csrfCross-Site 跨站请求伪造保护,由于这是一个仅用于演示目的的简单应用程序,我们现在可以安全地禁用它。
  • 然后我们添加需要对所有请求进行身份验证的配置。 正如我们稍后将看到的,为简单起见,我们将为此应用程序的索引页面设置一个"/"端点。
  • 之后,我们将使用上面提到的 Spring Security 的 formLogin() 功能。 这会生成一个简单的登录页面。
  • 然后,我们使用 Spring Security 的 rememberMe() 功能。 这将执行两件事。
    • 首先,它会在我们使用 formLogin() 生成的默认登录表单中添加一个"记住我"复选框。
    • 其次,勾选复选框会生成记住我的 cookie。 cookie 存储用户的身份,浏览器存储它。 Spring Security 在未来的会话中检测 cookie 以自动登录。

    因此,用户无需再次登录即可再次访问该应用程序。

  • 最后,我们有 logout() 功能。 为此,Spring security 也提供了一个默认功能。 在这里它执行两个重要的功能 −
    • 使 Http 会话无效,并取消绑定到会话的对象。
    • 它清除了记住我的 cookie。
    • 从 Spring 的 Security 上下文中删除身份验证。

    我们还提供了一个 logoutSuccessUrl(),以便应用程序在注销后返回登录页面。 这样就完成了我们的应用程序配置。

受保护的内容(可选)

我们现在将创建一个虚拟索引页面,供用户在登录时查看。它还将包含一个注销按钮。

在我们的 /src/main/resources/templates 中,我们添加一个 index.html 文件。然后向其中添加一些 Html 内容。

实例


<!doctype html> 
<html lang="en"> 
   <head> 
      <!-- Required meta tags -->
      <meta charset="utf-8"> 
      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 
      <!-- Bootstrap CSS --> 
      <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> 
      <title>Hello, world!</title> 
   </head> 
   <body> 
      <h1>Hello, world!</h1> <a href="logout">logout</a> 
      <!-- Optional JavaScript --> 
      <!-- jQuery first, then Popper.js, then Bootstrap JS --> 
      <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> 
      <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> 
      <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
      integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> 
   </body> 
</html>

此内容来自 Bootstrap 4 入门模板。

我们还添加了

<a href="logout">logout</a>

到我们的文件,以便用户可以使用此链接注销应用程序。

资源控制器

我们已经创建了受保护的资源,现在我们添加控制器来服务这个资源。

实例


package com.tutorial.spring.security.formlogin.controllers; 
import org.springframework.stereotype.Controller; 
import org.springframework.web.bind.annotation.GetMapping; 
@Controller public class AuthController { 
   @GetMapping("/") public String home() { return "index"; }
}

正如我们所见,它是一个非常简单的控制器。 它只有一个 get 端点,在启动我们的应用程序时为我们的 index.html 文件提供服务。

运行应用程序

让我们将应用程序作为 Spring Boot 应用程序运行。 当应用程序启动时,我们可以在浏览器上访问 http://localhost:8080。它应该要求我们输入用户名和密码。 此外,我们还可以看到记住我的复选框。

登录

Login Page

现在,如果我们提供我们在 WebSecurity 配置文件中配置的用户信息,我们将能够登录。此外,如果我们勾选记住我复选框,我们将能够在我们的 浏览器的开发者工具部分。

控制台应用程序 控制台网络

正如我们所见,cookie 是与我们的登录请求一起发送的。

此外,网页中还包含一个用于注销的链接。 单击链接后,我们将退出我们的应用程序并返回我们的登录页面。